diff --git a/packages-user/client-modules/src/render/components/choices.tsx b/packages-user/client-modules/src/render/components/choices.tsx index 277def6..8628606 100644 --- a/packages-user/client-modules/src/render/components/choices.tsx +++ b/packages-user/client-modules/src/render/components/choices.tsx @@ -139,11 +139,11 @@ export const ConfirmBox = defineComponent< height.value = textHeight + pad.value * 4; }; - const setYes = (_: string, width: number, height: number) => { + const setYes = (width: number, height: number) => { yesSize.value = [width, height]; }; - const setNo = (_: string, width: number, height: number) => { + const setNo = (width: number, height: number) => { noSize.value = [width, height]; }; @@ -190,7 +190,7 @@ export const ConfirmBox = defineComponent< zIndex={15} onClick={() => emit('yes')} onEnter={() => (selected.value = true)} - onSetText={setYes} + onResize={setYes} /> emit('no')} onEnter={() => (selected.value = false)} - onSetText={setNo} + onResize={setNo} /> ); @@ -459,7 +459,7 @@ export const Choices = defineComponent< contentHeight.value = height; }; - const updateTitleHeight = (_0: string, _1: number, height: number) => { + const updateTitleHeight = (_: number, height: number) => { titleHeight.value = height; }; @@ -519,7 +519,7 @@ export const Choices = defineComponent< font={props.titleFont ?? new Font(void 0, 18)} fillStyle={props.titleFill ?? 'gold'} zIndex={5} - onSetText={updateTitleHeight} + onResize={updateTitleHeight} /> emit('choose', v[0])} - onSetText={(_, width, height) => + onResize={(width, height) => updateChoiceSize(i, width, height) } onEnter={() => (selected.value = i)} diff --git a/packages-user/client-modules/src/render/components/floorSelect.tsx b/packages-user/client-modules/src/render/components/floorSelect.tsx index 30a67a5..0d726a3 100644 --- a/packages-user/client-modules/src/render/components/floorSelect.tsx +++ b/packages-user/client-modules/src/render/components/floorSelect.tsx @@ -219,7 +219,7 @@ export const FloorSelector = defineComponent< lineWidth={1} strokeStyle="#aaa" /> - ( if (!ele) return; // 计算当前绝对位置 - const chain: RenderItem[] = []; - let now: RenderItem | undefined = root.value; - let renderer: MotaRenderer | undefined; + const chain: IRenderItem[] = []; + let now: IRenderItem | null = root.value ?? null; + let renderer: IRenderTreeRoot | null = null; if (!now) return; while (now) { chain.unshift(now); @@ -441,11 +442,11 @@ export const InputBox = defineComponent< emit('input', value); }; - const setYes = (_: string, width: number, height: number) => { + const setYes = (width: number, height: number) => { yesSize.value = [width, height]; }; - const setNo = (_: string, width: number, height: number) => { + const setNo = (width: number, height: number) => { noSize.value = [width, height]; }; @@ -500,7 +501,7 @@ export const InputBox = defineComponent< zIndex={15} onClick={confirm} onEnter={() => (selected.value = true)} - onSetText={setYes} + onResize={setYes} /> (selected.value = false)} - onSetText={setNo} + onResize={setNo} /> ); diff --git a/packages-user/client-modules/src/render/components/misc.tsx b/packages-user/client-modules/src/render/components/misc.tsx index e28e0e4..440ef02 100644 --- a/packages-user/client-modules/src/render/components/misc.tsx +++ b/packages-user/client-modules/src/render/components/misc.tsx @@ -1,13 +1,18 @@ -import { ElementLocator, Sprite, MotaOffscreenCanvas2D } from '@motajs/render'; -import { DefaultProps, PathProps, onTick } from '@motajs/render-vue'; +import { + ElementLocator, + CustomRenderItem, + MotaOffscreenCanvas2D +} from '@motajs/render'; +import { DefaultProps, PathProps } from '@motajs/render-vue'; import { computed, defineComponent, ref, SetupContext, watch } from 'vue'; import { TextContent, TextContentProps } from './textbox'; import { Scroll, ScrollExpose, ScrollProps } from './scroll'; import { transitioned } from '../use'; -import { hyper } from 'mutate-animate'; import { logger } from '@motajs/common'; import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system'; import { clamp } from 'lodash-es'; +import { using } from '../renderer'; +import { cosh, CurveMode } from '@motajs/animate'; interface ProgressProps extends DefaultProps { /** 进度条的位置 */ @@ -37,7 +42,7 @@ const progressProps = { * ``` */ export const Progress = defineComponent(props => { - const element = ref(); + const element = ref(); const render = (canvas: MotaOffscreenCanvas2D) => { const { ctx } = canvas; @@ -67,7 +72,7 @@ export const Progress = defineComponent(props => { }); return () => { - return ; + return ; }; }, progressProps); @@ -217,7 +222,7 @@ export const ScrollText = defineComponent< let paused = false; let nowScroll = 0; - onTick(() => { + using.onExcitedFunc(() => { if (paused || !scroll.value) return; const now = Date.now(); const dt = now - lastFixedTime; @@ -296,7 +301,11 @@ const selectionProps = { export const Selection = defineComponent(props => { const minAlpha = computed(() => props.alphaRange?.[0] ?? 0.25); const maxAlpha = computed(() => props.alphaRange?.[1] ?? 0.55); - const alpha = transitioned(minAlpha.value, 2000, hyper('sin', 'in-out'))!; + const alpha = transitioned( + minAlpha.value, + 2000, + cosh(2, CurveMode.EaseInOut) + )!; const isWinskin = computed(() => !!props.winskin); const winskinImage = computed(() => @@ -326,7 +335,7 @@ export const Selection = defineComponent(props => { ctx.drawImage(image, 158, 66, 2, 28, width - 2, 2, 2, height - 4); }; - onTick(() => { + using.onExcitedFunc(() => { if (alpha.value === maxAlpha.value) { alpha.set(minAlpha.value); } @@ -337,7 +346,7 @@ export const Selection = defineComponent(props => { return () => isWinskin.value ? ( - (props => { return () => isWinskin.value ? ( - + ) : ( (props => { }, backgroundProps); export interface WaitBoxProps - extends Partial, - Partial { + extends Partial, Partial { loc: ElementLocator; width: number; promise?: Promise; diff --git a/packages-user/client-modules/src/render/components/scroll.tsx b/packages-user/client-modules/src/render/components/scroll.tsx index b5222d9..95265c8 100644 --- a/packages-user/client-modules/src/render/components/scroll.tsx +++ b/packages-user/client-modules/src/render/components/scroll.tsx @@ -13,8 +13,7 @@ import { import { Container, ElementLocator, - RenderItem, - Sprite, + CustomRenderItem, Transform, MotaOffscreenCanvas2D, IActionEvent, @@ -22,9 +21,10 @@ import { MouseType, EventProgress, ActionEventMap, - ContainerCustom, ActionType, - CustomContainerPropagateOrigin + CustomContainerPropagateOrigin, + IRenderItem, + ICustomContainer } from '@motajs/render'; import { hyper, linear, Transition } from 'mutate-animate'; import { clamp } from 'lodash-es'; @@ -110,10 +110,10 @@ export const Scroll = defineComponent( /** 滚动条的定位 */ const sp = ref([0, 0, 1, 1]); - const listenedChild: Set = new Set(); - const areaMap: Map = new Map(); + const listenedChild: Set = new Set(); + const areaMap: Map = new Map(); const content = ref(); - const scroll = ref(); + const scroll = ref(); const scrollAlpha = transitioned(0.5, 100, linear())!; @@ -187,7 +187,7 @@ export const Scroll = defineComponent( /** * 计算一个元素会在画面上显示的区域 */ - const getArea = (item: RenderItem, rect: DOMRectReadOnly) => { + const getArea = (item: IRenderItem, rect: DOMRectReadOnly) => { if (direction.value === ScrollDirection.Horizontal) { areaMap.set(item, [rect.left - width.value, rect.right]); } else { @@ -198,7 +198,7 @@ export const Scroll = defineComponent( /** * 检查一个元素是否需要显示,不需要则隐藏 */ - const checkItem = (item: RenderItem) => { + const checkItem = (item: IRenderItem) => { const area = areaMap.get(item); if (!area) { item.show(); @@ -222,7 +222,7 @@ export const Scroll = defineComponent( /** * 当一个元素的矩阵发生变换时执行,检查其显示区域 */ - const onTransform = (item: RenderItem) => { + const onTransform = (item: IRenderItem) => { const rect = item.getBoundingRect(); const pad = props.padEnd ?? 0; if (item.parent === content.value) { @@ -340,7 +340,7 @@ export const Scroll = defineComponent( const renderContent = ( canvas: MotaOffscreenCanvas2D, - children: RenderItem[], + children: IRenderItem[], transform: Transform ) => { const ctx = canvas.ctx; @@ -367,7 +367,7 @@ export const Scroll = defineComponent( type: T, progress: EventProgress, event: ActionEventMap[T], - _: ContainerCustom, + _: ICustomContainer, origin: CustomContainerPropagateOrigin ) => { if (progress === EventProgress.Capture) { @@ -562,7 +562,7 @@ export const Scroll = defineComponent( > {slots.default?.()} - + > ); }; diff --git a/packages-user/client-modules/src/render/components/textbox.tsx b/packages-user/client-modules/src/render/components/textbox.tsx index 36eeca3..b17e988 100644 --- a/packages-user/client-modules/src/render/components/textbox.tsx +++ b/packages-user/client-modules/src/render/components/textbox.tsx @@ -1,7 +1,7 @@ import { ElementLocator, Font, - Sprite, + CustomRenderItem, Text, MotaOffscreenCanvas2D } from '@motajs/render'; @@ -171,7 +171,7 @@ export const TextContent = defineComponent< expose({ retype, showAll, getHeight }); - const spriteElement = shallowRef(); + const spriteElement = shallowRef(); const renderContent = (canvas: MotaOffscreenCanvas2D) => { const ctx = canvas.ctx; ctx.textBaseline = 'top'; @@ -225,11 +225,11 @@ export const TextContent = defineComponent< return () => { return ( - + > ); }; }, textContentOptions); @@ -493,7 +493,7 @@ export const Textbox = defineComponent< slots.title(data) ) : props.winskin ? ( ) : ( @@ -514,7 +514,7 @@ export const Textbox = defineComponent< slots.default(data) ) : props.winskin ? ( ) : ( diff --git a/packages-user/client-modules/src/render/components/textboxTyper.ts b/packages-user/client-modules/src/render/components/textboxTyper.ts index c20f124..6b3242f 100644 --- a/packages-user/client-modules/src/render/components/textboxTyper.ts +++ b/packages-user/client-modules/src/render/components/textboxTyper.ts @@ -3,7 +3,7 @@ import { Font, MotaOffscreenCanvas2D } from '@motajs/render'; import EventEmitter from 'eventemitter3'; import { isNil } from 'lodash-es'; import { RenderableData, AutotileRenderable, texture } from '../elements'; -import { onTick } from '@motajs/render-vue'; +import { using } from '../renderer'; /** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */ const SAFE_PAD = 1; @@ -151,8 +151,7 @@ interface ISizedTextContentBlock { } export interface ITextContentTextBlock - extends ITextContentBlockBase, - ISizedTextContentBlock { + extends ITextContentBlockBase, ISizedTextContentBlock { readonly type: TextContentType.Text; /** 文本 block 的文字内容 */ readonly text: string; @@ -165,8 +164,7 @@ export interface ITextContentTextBlock } export interface ITextContentIconBlock - extends ITextContentBlockBase, - ISizedTextContentBlock { + extends ITextContentBlockBase, ISizedTextContentBlock { readonly type: TextContentType.Icon; /** 图标 block 显示的图标 */ readonly icon: AllNumbers; @@ -200,8 +198,7 @@ export interface ITyperRenderableBase { } export interface ITyperTextRenderable - extends ITextContentTextBlock, - ITyperRenderableBase { + extends ITextContentTextBlock, ITyperRenderableBase { /** 文本左上角的横坐标 */ readonly x: number; /** 文本左上角的纵坐标 */ @@ -211,8 +208,7 @@ export interface ITyperTextRenderable } export interface ITyperIconRenderable - extends ITextContentIconBlock, - ITyperRenderableBase { + extends ITextContentIconBlock, ITyperRenderableBase { /** 图标左上角的横坐标 */ readonly x: number; /** 图标左上角的纵坐标 */ @@ -220,8 +216,7 @@ export interface ITyperIconRenderable } export interface ITyperWaitRenderable - extends ITextContentWaitBlock, - ITyperRenderableBase { + extends ITextContentWaitBlock, ITyperRenderableBase { /** 当然是否已经等待了多少个字符 */ waited: number; } @@ -314,7 +309,7 @@ export class TextContentTyper extends EventEmitter { this.config ); - onTick(() => this.tick()); + using.onExcitedFunc(() => this.tick()); } /** diff --git a/packages-user/client-modules/src/render/components/thumbnail.tsx b/packages-user/client-modules/src/render/components/thumbnail.tsx index ecab03f..cd76315 100644 --- a/packages-user/client-modules/src/render/components/thumbnail.tsx +++ b/packages-user/client-modules/src/render/components/thumbnail.tsx @@ -1,9 +1,13 @@ -import { ElementLocator, MotaOffscreenCanvas2D, Sprite } from '@motajs/render'; -import { SpriteProps } from '@motajs/render-vue'; +import { + ElementLocator, + MotaOffscreenCanvas2D, + CustomRenderItem +} from '@motajs/render'; +import { CustomProps } from '@motajs/render-vue'; import { defineComponent, ref, watch } from 'vue'; import { SetupComponentOptions } from '@motajs/system'; -export interface ThumbnailProps extends SpriteProps { +export interface ThumbnailProps extends CustomProps { /** 缩略图的位置 */ loc: ElementLocator; /** 楼层 ID */ @@ -37,7 +41,7 @@ const thumbnailProps = { } satisfies SetupComponentOptions; export const Thumbnail = defineComponent(props => { - const spriteRef = ref(); + const spriteRef = ref(); const update = () => { spriteRef.value?.update(); @@ -75,6 +79,6 @@ export const Thumbnail = defineComponent(props => { watch(props, update); return () => ( - + ); }, thumbnailProps); diff --git a/packages-user/client-modules/src/render/components/tip.tsx b/packages-user/client-modules/src/render/components/tip.tsx index f1e3204..7d8bdb0 100644 --- a/packages-user/client-modules/src/render/components/tip.tsx +++ b/packages-user/client-modules/src/render/components/tip.tsx @@ -106,7 +106,7 @@ export const Tip = defineComponent((props, { expose }) => { hide(); }; - const onSetText = (_: string, width: number) => { + const onSetText = (width: number) => { textWidth.value = width; }; @@ -137,7 +137,7 @@ export const Tip = defineComponent((props, { expose }) => { diff --git a/packages-user/client-modules/src/render/elements/animate.ts b/packages-user/client-modules/src/render/elements/animate.ts deleted file mode 100644 index b421af2..0000000 --- a/packages-user/client-modules/src/render/elements/animate.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { - RenderAdapter, - transformCanvas, - ERenderItemEvent, - RenderItem, - Transform, - MotaOffscreenCanvas2D -} from '@motajs/render'; -import { HeroRenderer } from './hero'; -import { ILayerGroupRenderExtends, LayerGroup } from './layer'; - -export class LayerGroupAnimate implements ILayerGroupRenderExtends { - static animateList: Set = new Set(); - id: string = 'animate'; - - group!: LayerGroup; - hero?: HeroRenderer; - animate!: Animate; - - private animation: Set = new Set(); - - /** - * 绘制一个跟随勇士的动画 - * @param name 动画id - */ - async drawHeroAnimate(name: AnimationIds) { - const animate = this.animate.animate(name, 0, 0); - this.updatePosition(animate); - await this.animate.draw(animate); - this.animation.delete(animate); - } - - private updatePosition(animate: AnimateData) { - if (!this.checkHero()) return; - if (!this.hero?.renderable) return; - const { x, y } = this.hero.renderable; - const cell = this.group.cellSize; - const half = cell / 2; - animate.centerX = x * cell + half; - animate.centerY = y * cell + half; - } - - private onMoveTick = (x: number, y: number) => { - const cell = this.group.cellSize; - const half = cell / 2; - const ax = x * cell + half; - const ay = y * cell + half; - this.animation.forEach(v => { - v.centerX = ax; - v.centerY = ay; - }); - }; - - private listen() { - if (this.checkHero()) { - this.hero!.on('moveTick', this.onMoveTick); - } - } - - private checkHero() { - if (this.hero) return true; - const ex = this.group.getLayer('event')?.getExtends('floor-hero'); - if (ex instanceof HeroRenderer) { - this.hero = ex; - return true; - } - return false; - } - - awake(group: LayerGroup): void { - this.group = group; - this.animate = new Animate(); - this.animate.size(group.width, group.height); - this.animate.setHD(true); - this.animate.setZIndex(100); - group.appendChild(this.animate); - LayerGroupAnimate.animateList.add(this); - this.listen(); - } - - onDestroy(_group: LayerGroup): void { - if (this.checkHero()) { - this.hero!.off('moveTick', this.onMoveTick); - LayerGroupAnimate.animateList.delete(this); - } - } -} - -interface AnimateData { - obj: globalThis.Animate; - /** 第一帧是全局第几帧 */ - readonly start: number; - /** 当前是第几帧 */ - index: number; - /** 是否需要播放音频 */ - sound: boolean; - centerX: number; - centerY: number; - onEnd?: () => void; - readonly absolute: boolean; -} - -export interface EAnimateEvent extends ERenderItemEvent {} - -export class Animate extends RenderItem { - /** 绝对位置的动画 */ - private absoluteAnimates: Set = new Set(); - /** 静态位置的动画 */ - private staticAnimates: Set = new Set(); - - private delegation: number; - private frame: number = 0; - private lastTime: number = 0; - - constructor() { - super('absolute', false, true); - - this.delegation = this.delegateTicker(time => { - if (time - this.lastTime < 50) return; - this.lastTime = time; - this.frame++; - if ( - this.absoluteAnimates.size > 0 || - this.staticAnimates.size > 0 - ) { - this.update(this); - } - }); - - adapter.add(this); - } - - protected render( - canvas: MotaOffscreenCanvas2D, - transform: Transform - ): void { - if ( - this.absoluteAnimates.size === 0 && - this.staticAnimates.size === 0 - ) { - return; - } - this.drawAnimates(this.absoluteAnimates, canvas); - transformCanvas(canvas, transform); - this.drawAnimates(this.staticAnimates, canvas); - } - - private drawAnimates( - data: Set, - canvas: MotaOffscreenCanvas2D - ) { - if (data.size === 0) return; - const { ctx } = canvas; - const toDelete = new Set(); - data.forEach(v => { - const obj = v.obj; - const index = v.index; - const frame = obj.frames[index]; - const ratio = obj.ratio; - if (!v.sound) { - const se = (index % obj.frame) + 1; - core.playSound(v.obj.se[se], v.obj.pitch[se]); - v.sound = true; - } - const centerX = v.centerX; - const centerY = v.centerY; - - frame.forEach(v => { - const img = obj.images[v.index]; - if (!img) return; - - const realWidth = (img.width * ratio * v.zoom) / 100; - const realHeight = (img.height * ratio * v.zoom) / 100; - ctx.globalAlpha = v.opacity / 255; - - const cx = centerX + v.x; - const cy = centerY + v.y; - - const ix = -realWidth / 2; - const iy = -realHeight / 2; - const angle = v.angle ? (-v.angle * Math.PI) / 180 : 0; - - ctx.save(); - ctx.translate(cx, cy); - if (v.mirror) { - ctx.scale(-1, 1); - } - ctx.rotate(angle); - ctx.drawImage(img, ix, iy, realWidth, realHeight); - ctx.restore(); - }); - const now = this.frame - v.start; - if (now !== v.index) v.sound = true; - v.index = now; - if (v.index === v.obj.frame) { - toDelete.add(v); - } - }); - toDelete.forEach(v => { - data.delete(v); - v.onEnd?.(); - }); - } - - /** - * 创建一个可以被执行的动画 - * @param name 动画名称 - * @param absolute 是否是绝对定位,绝对定位不会受到transform的影响 - */ - animate( - name: AnimationIds, - x: number, - y: number, - absolute: boolean = false - ) { - const animate = core.material.animates[name]; - const data: AnimateData = { - index: 0, - start: this.frame, - obj: animate, - centerX: x, - centerY: y, - absolute, - sound: false - }; - return data; - } - - /** - * 绘制动画,动画结束时兑现返回的Promise - * @param animate 动画信息 - * @returns - */ - draw(animate: AnimateData): Promise { - return new Promise(res => { - if (animate.absolute) { - this.absoluteAnimates.add(animate); - } else { - this.staticAnimates.add(animate); - } - animate.onEnd = () => { - res(); - }; - }); - } - - /** - * 根据动画名称、坐标、定位绘制动画 - * @param name 动画名称 - * @param absolute 是否是绝对定位 - */ - drawAnimate( - name: AnimationIds, - x: number, - y: number, - absolute: boolean = false - ) { - return this.draw(this.animate(name, x, y, absolute)); - } - - destroy(): void { - super.destroy(); - this.removeTicker(this.delegation); - adapter.remove(this); - } -} - -const adapter = new RenderAdapter('animate'); -adapter.receive('drawAnimate', (item, name, x, y, absolute) => { - return item.drawAnimate(name, x, y, absolute); -}); -adapter.receiveGlobal('drawHeroAnimate', name => { - const execute: Promise[] = []; - LayerGroupAnimate.animateList.forEach(v => { - execute.push(v.drawHeroAnimate(name)); - }); - return Promise.all(execute); -}); diff --git a/packages-user/client-modules/src/render/elements/block.ts b/packages-user/client-modules/src/render/elements/block.ts deleted file mode 100644 index 94cd2b9..0000000 --- a/packages-user/client-modules/src/render/elements/block.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { EventEmitter } from 'eventemitter3'; -import { logger } from '@motajs/common'; -import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render'; - -interface BlockCacherEvent { - split: []; - beforeClear: [index: number]; -} - -interface BlockData { - /** 横向宽度,包括rest的那一个块 */ - width: number; - /** 纵向宽度,包括rest的那一个块 */ - height: number; - /** 横向最后一个块的宽度 */ - restWidth: number; - /** 纵向最后一个块的高度 */ - restHeight: number; -} - -export interface IBlockCacheable { - /** - * 摧毁这个缓存元素 - */ - destroy(): void; -} - -/** - * 简单分块缓存类,内容包含元素与分块两种,其中元素是最小单元,分块是缓存单元。 - * 拿楼层举例,假如我将楼层按照13x13划分缓存,那么元素就是每个图块,而分块就是这13x13的缓存分块。 - * 为方便区分,在相关函数的注释最后,都会有`xx -> yy`的说明, - * 其中xx说明传入的数据是元素还是分块的数据,而yy表示其返回值或转换为的值 - */ -export class BlockCacher< - T extends IBlockCacheable -> extends EventEmitter { - /** 区域宽度 */ - width: number; - /** 区域高度 */ - height: number; - /** 区域面积 */ - area: number = 0; - /** 分块大小 */ - blockSize: number; - /** 分块信息 */ - blockData: BlockData = { - width: 0, - height: 0, - restWidth: 0, - restHeight: 0 - }; - /** 缓存深度,例如填4的时候表示每格包含4个缓存 */ - cacheDepth: number = 1; - - /** 缓存内容,计算公式为 (x + y * width) * depth + deep */ - cache: Map = new Map(); - - constructor( - width: number, - height: number, - size: number, - depth: number = 1 - ) { - super(); - this.width = width; - this.height = height; - this.blockSize = size; - this.cacheDepth = depth; - this.split(); - } - - /** - * 设置区域大小 - * @param width 区域宽度 - * @param height 区域高度 - */ - size(width: number, height: number) { - this.width = width; - this.height = height; - this.split(); - } - - /** - * 设置分块大小 - */ - setBlockSize(size: number) { - this.blockSize = size; - this.split(); - } - - /** - * 设置缓存深度,设置后会自动将旧缓存移植到新缓存中,最大值为31 - * @param depth 缓存深度 - */ - setCacheDepth(depth: number) { - if (depth > 31) { - logger.error(11); - return; - } - const old = this.cache; - const before = this.cacheDepth; - this.cache = new Map(); - old.forEach((v, k) => { - const index = Math.floor(k / before); - const deep = k % before; - this.cache.set(index * depth + deep, v); - }); - old.clear(); - this.cacheDepth = depth; - } - - /** - * 执行分块 - */ - split() { - this.blockData = { - width: Math.ceil(this.width / this.blockSize), - height: Math.ceil(this.height / this.blockSize), - restWidth: this.width % this.blockSize, - restHeight: this.height % this.blockSize - }; - this.area = this.blockData.width * this.blockData.height; - this.emit('split'); - } - - /** - * 清除指定块的索引(分块->void) - * @param index 要清除的缓存索引 - * @param deep 清除哪些深度的缓存,至多31位二进制数,例如填0b111就是清除前三层的索引 - */ - clearCache(index: number, deep: number) { - const depth = this.cacheDepth; - for (let i = 0; i < depth; i++) { - if (deep & (1 << i)) { - const nowIndex = index * this.cacheDepth + i; - const item = this.cache.get(nowIndex); - item?.destroy(); - this.cache.delete(nowIndex); - } - } - } - - /** - * 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存(元素->void) - */ - clearCacheByIndex(index: number) { - const item = this.cache.get(index); - item?.destroy(); - this.cache.delete(index); - } - - /** - * 清空所有缓存 - */ - clearAllCache() { - this.cache.forEach(v => v.destroy()); - this.cache.clear(); - } - - /** - * 根据分块的横纵坐标获取其索引(分块->分块) - */ - getIndex(x: number, y: number) { - return x + y * this.blockData.width; - } - - /** - * 根据元素位置获取分块索引(注意是单个元素的位置,而不是分块的位置)(元素->分块) - */ - getIndexByLoc(x: number, y: number) { - return this.getIndex( - Math.floor(x / this.blockSize), - Math.floor(y / this.blockSize) - ); - } - - /** - * 根据块的索引获取其位置(分块->分块) - */ - getBlockXYByIndex(index: number): LocArr { - const width = this.blockData.width; - return [index % width, Math.floor(index / width)]; - } - - /** - * 获取一个元素位置所在的分块位置(即使它不在任何分块内)(元素->分块) - */ - getBlockXY(x: number, y: number): LocArr { - return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)]; - } - - /** - * 根据分块坐标与deep获取一个分块的精确索引(分块->分块) - */ - getPreciseIndex(x: number, y: number, deep: number) { - return (x + y * this.blockSize) * this.cacheDepth + deep; - } - - /** - * 根据元素坐标及deep获取元素所在块的精确索引(元素->分块) - */ - getPreciseIndexByLoc(x: number, y: number, deep: number) { - return this.getPreciseIndex(...this.getBlockXY(x, y), deep); - } - - /** - * 更新指定元素区域内的缓存(注意坐标是元素坐标,而非分块坐标)(元素->分块) - * @param deep 缓存清除深度,默认全部清空 - * @returns 更新区域内的所有有关分块索引 - */ - updateElementArea( - x: number, - y: number, - width: number, - height: number, - deep: number = 2 ** 31 - 1 - ) { - const [bx, by] = this.getBlockXY(x, y); - const [ex, ey] = this.getBlockXY(x + width - 1, y + height - 1); - - return this.updateArea(bx, by, ex - bx, ey - by, deep); - } - - /** - * 更新指定分块区域内的缓存(注意坐标是分块坐标,而非元素坐标)(分块->分块) - * @param deep 缓存清除深度,默认全部清空 - * @returns 更新区域内的所有分块索引 - */ - updateArea( - x: number, - y: number, - width: number, - height: number, - deep: number = 2 ** 31 - 1 - ) { - const blocks = this.getIndexOf(x, y, width, height); - - blocks.forEach(v => { - this.clearCache(v, deep); - }); - return blocks; - } - - /** - * 传入分块坐标与范围,获取该区域内包含的所有分块索引(分块->分块) - */ - getIndexOf(x: number, y: number, width: number, height: number) { - const res = new Set(); - const sx = Math.max(x, 0); - const sy = Math.max(y, 0); - const ex = Math.min(x + width, this.blockData.width); - const ey = Math.min(y + height, this.blockData.height); - - for (let nx = sx; nx <= ex; nx++) { - for (let ny = sy; ny <= ey; ny++) { - const index = this.getIndex(nx, ny); - res.add(index); - } - } - - return res; - } - - /** - * 传入元素坐标与范围,获取该区域内包含的所有分块索引(元素->分块) - */ - getIndexOfElement(x: number, y: number, width: number, height: number) { - const [bx, by] = this.getBlockXY(x, y); - const [ex, ey] = this.getBlockXY(x + width, y + height); - return this.getIndexOf(bx, by, ex - bx, ey - by); - } - - /** - * 根据分块索引,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素) - * @param block 分块索引 - */ - getRectOfIndex(block: number) { - const [x, y] = this.getBlockXYByIndex(block); - return this.getRectOfBlockXY(x, y); - } - - /** - * 根据分块坐标,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素) - * @param x 分块横坐标 - * @param y 分块纵坐标 - */ - getRectOfBlockXY(x: number, y: number) { - return [ - x * this.blockSize, - y * this.blockSize, - (x + 1) * this.blockSize, - (y + 1) * this.blockSize - ]; - } - - /** - * 摧毁这个块缓存 - */ - destroy() { - this.clearAllCache(); - } -} - -export interface ICanvasCacheItem extends IBlockCacheable { - readonly canvas: MotaOffscreenCanvas2D; - symbol: number; -} - -export class CanvasCacheItem implements ICanvasCacheItem { - constructor( - public readonly canvas: MotaOffscreenCanvas2D, - public readonly symbol: number, - public readonly element: RenderItem - ) {} - - destroy(): void { - this.element.deleteCanvas(this.canvas); - } -} diff --git a/packages-user/client-modules/src/render/elements/camera.ts b/packages-user/client-modules/src/render/elements/camera.ts deleted file mode 100644 index 0ef58d9..0000000 --- a/packages-user/client-modules/src/render/elements/camera.ts +++ /dev/null @@ -1,624 +0,0 @@ -import { Animation, TimingFn, Transition } from 'mutate-animate'; -import { RenderItem, Transform } from '@motajs/render'; -import { logger } from '@motajs/common'; -import EventEmitter from 'eventemitter3'; - -export interface ICameraTranslate { - readonly type: 'translate'; - readonly from: Camera; - x: number; - y: number; -} - -export interface ICameraRotate { - readonly type: 'rotate'; - readonly from: Camera; - /** 旋转角,单位弧度 */ - angle: number; -} - -export interface ICameraScale { - readonly type: 'scale'; - readonly from: Camera; - x: number; - y: number; -} - -type CameraOperation = ICameraTranslate | ICameraScale | ICameraRotate; - -interface CameraEvent { - destroy: []; -} - -export class Camera extends EventEmitter { - /** 当前绑定的渲染元素 */ - readonly binded: RenderItem; - /** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */ - transform: Transform; - - /** 委托ticker的id */ - private delegation: number; - /** 所有的动画id */ - private animationIds: Set = new Set(); - - /** 是否需要更新视角 */ - private needUpdate: boolean = false; - /** 是否启用摄像机 */ - private enabled: boolean = true; - - /** 变换操作列表,因为矩阵乘法跟顺序有关,因此需要把各个操作拆分成列表进行 */ - protected operation: CameraOperation[] = []; - - /** 渲染元素到摄像机的映射 */ - private static cameraMap: Map = new Map(); - - /** - * 获取一个渲染元素的摄像机,如果不存在则为它创建一个并返回。注意使用`new Camera`创建的摄像机不在此列 - * @param item 渲染元素 - */ - static for(item: RenderItem) { - const camera = this.cameraMap.get(item); - if (!camera) { - const ca = new Camera(item); - this.cameraMap.set(item, ca); - return ca; - } else { - return camera; - } - } - - constructor(item: RenderItem) { - super(); - - this.binded = item; - - this.delegation = item.delegateTicker(() => this.tick()); - this.transform = item.transform; - - item.on('destroy', () => { - this.destroy(); - }); - - const ca = Camera.cameraMap.get(item); - if (ca && ca.enabled) { - logger.warn(22); - } - } - - private tick = () => { - if (!this.needUpdate || !this.enabled) return; - const trans = this.transform; - trans.reset(); - for (const o of this.operation) { - if (o.type === 'translate') { - trans.translate(-o.x, -o.y); - } else if (o.type === 'rotate') { - trans.rotate(o.angle); - } else { - trans.scale(o.x, o.y); - } - } - this.binded.update(this.binded); - this.needUpdate = false; - }; - - /** - * 禁用这个摄像机 - */ - disable() { - this.enabled = false; - } - - /** - * 启用这个摄像机 - */ - enable() { - this.enabled = true; - } - - /** - * 在下一帧进行强制更新 - */ - requestUpdate() { - this.needUpdate = true; - } - - /** - * 移除一个变换操作 - * @param operation 要移除的操作 - */ - removeOperation(operation: CameraOperation) { - const index = this.operation.indexOf(operation); - if (index === -1) return; - this.operation.splice(index, 1); - } - - /** - * 清空变换操作列表 - */ - clearOperation() { - this.operation.splice(0); - } - - /** - * 添加一个平移操作 - * @returns 添加的平移变换操作 - */ - addTranslate(): ICameraTranslate { - const item: ICameraTranslate = { - type: 'translate', - x: 0, - y: 0, - from: this - }; - this.operation.push(item); - return item; - } - - /** - * 添加一个旋转操作 - * @returns 添加的旋转变换操作 - */ - addRotate(): ICameraRotate { - const item: ICameraRotate = { - type: 'rotate', - angle: 0, - from: this - }; - this.operation.push(item); - return item; - } - - /** - * 添加一个放缩操作 - * @returns 添加的放缩变换操作 - */ - addScale(): ICameraScale { - const item: ICameraScale = { - type: 'scale', - x: 1, - y: 1, - from: this - }; - this.operation.push(item); - return item; - } - - /** - * 施加动画 - * @param time 动画时长 - * @param update 每帧的更新函数 - */ - applyAnimation(time: number, update: () => void) { - const delegation = this.binded.delegateTicker( - () => { - update(); - this.needUpdate = true; - }, - time, - () => { - update(); - this.needUpdate = true; - this.animationIds.delete(delegation); - } - ); - this.animationIds.add(delegation); - } - - /** - * 为一个平移操作实施动画 - * @param operation 平移操作 - * @param animate 动画实例 - * @param time 动画时长 - */ - applyTranslateAnimation( - operation: ICameraTranslate, - animate: Animation, - time: number - ) { - if (operation.from !== this) { - logger.warn(20); - return; - } - - const update = () => { - operation.x = animate.x; - operation.y = animate.y; - }; - - this.applyAnimation(time, update); - } - - /** - * 为一个旋转操作实施动画 - * @param operation 旋转操作 - * @param animate 动画实例 - * @param time 动画时长 - */ - applyRotateAnimation( - operation: ICameraRotate, - animate: Animation, - time: number - ) { - if (operation.from !== this) { - logger.warn(20); - return; - } - - const update = () => { - operation.angle = animate.angle; - }; - - this.applyAnimation(time, update); - } - - /** - * 为一个缩放操作实施动画 - * @param operation 缩放操作 - * @param animate 动画实例 - * @param time 动画时长 - */ - applyScaleAnimation( - operation: ICameraScale, - animate: Animation, - time: number - ) { - if (operation.from !== this) { - logger.warn(20); - return; - } - - const update = () => { - operation.x = animate.size; - operation.y = animate.size; - }; - - this.applyAnimation(time, update); - } - - /** - * 为一个平移操作实施渐变,使用渐变的 x,y 值,即`transition.value.x`与`transition.value.y` - * @param operation 平移操作 - * @param animate 渐变实例 - * @param time 渐变时长 - */ - applyTranslateTransition( - operation: ICameraTranslate, - animate: Transition, - time: number - ) { - if (operation.from !== this) { - logger.warn(21); - return; - } - - const update = () => { - operation.x = animate.value.x; - operation.y = animate.value.y; - }; - - this.applyAnimation(time, update); - } - - /** - * 为一个旋转操作实施渐变,使用渐变的 angle 值,即`transition.value.angle` - * @param operation 旋转操作 - * @param animate 渐变实例 - * @param time 渐变时长 - */ - applyRotateTransition( - operation: ICameraRotate, - animate: Transition, - time: number - ) { - if (operation.from !== this) { - logger.warn(21); - return; - } - - const update = () => { - operation.angle = animate.value.angle; - }; - - this.applyAnimation(time, update); - } - - /** - * 为一个缩放操作实施渐变,使用渐变的 size 值,即`transition.value.size` - * @param operation 缩放操作 - * @param animate 渐变实例 - * @param time 渐变时长 - */ - applyScaleTransition( - operation: ICameraScale, - animate: Transition, - time: number - ) { - if (operation.from !== this) { - logger.warn(21); - return; - } - - const update = () => { - operation.x = animate.value.size; - operation.y = animate.value.size; - }; - - this.applyAnimation(time, update); - } - - /** - * 停止所有动画 - */ - stopAllAnimates() { - this.animationIds.forEach(v => this.binded.removeTicker(v)); - } - - /** - * 摧毁这个摄像机,当绑定元素被摧毁之后摄像机会一并摧毁,如果这个摄像机不使用了,一定要将它摧毁 - */ - destroy() { - this.binded.removeTicker(this.delegation); - this.animationIds.forEach(v => this.binded.removeTicker(v)); - Camera.cameraMap.delete(this.binded); - this.emit('destroy'); - } -} - -interface CameraAnimationBase { - type: string; - time: number; - start: number; -} - -export interface TranslateAnimation extends CameraAnimationBase { - type: 'translate'; - timing: TimingFn; - x: number; - y: number; -} - -export interface TranslateAsAnimation extends CameraAnimationBase { - type: 'translateAs'; - timing: TimingFn<2>; - time: number; -} - -export interface RotateAnimation extends CameraAnimationBase { - type: 'rotate'; - timing: TimingFn; - angle: number; - time: number; -} - -export interface ScaleAnimation extends CameraAnimationBase { - type: 'scale'; - timing: TimingFn; - scale: number; - time: number; -} - -export type CameraAnimationData = - | TranslateAnimation - | TranslateAsAnimation - | RotateAnimation - | ScaleAnimation; - -export interface CameraAnimationExecution { - data: CameraAnimationData[]; - animation: Animation; -} - -interface CameraAnimationEvent { - animate: [ - operation: CameraOperation, - execution: CameraAnimationExecution, - item: CameraAnimationData - ]; -} - -export class CameraAnimation extends EventEmitter { - camera: Camera; - - /** 动画开始时刻 */ - private startTime: number = 0; - /** 动画结束时刻 */ - private endTime: number = 0; - /** 委托ticker的id */ - private delegation: number; - /** 动画是否开始 */ - private started: boolean = false; - - /** 每个摄像机操作的动画映射 */ - private animateMap: Map = - new Map(); - - constructor(camera: Camera) { - super(); - - this.camera = camera; - this.delegation = camera.binded.delegateTicker(this.tick); - } - - private tick = () => { - if (!this.started) return; - const now = Date.now(); - const time = now - this.startTime; - if (now - this.startTime > this.endTime + 50) { - this.destroy(); - return; - } - this.animateMap.forEach((exe, ope) => { - const data = exe.data; - if (data.length === 0) return; - const item = data[0]; - if (item.start < time) { - this.executeAnimate(exe, item); - data.shift(); - this.emit('animate', ope, exe, item); - } - }); - this.camera.requestUpdate(); - }; - - private executeAnimate( - execution: CameraAnimationExecution, - animate: CameraAnimationData - ) { - if (animate.type === 'translateAs') { - const ani = this.ensureAnimate(execution); - ani.time(animate.time).moveAs(animate.timing); - } else if (animate.type === 'translate') { - const ani = this.ensureAnimate(execution); - const { x, y, time, timing } = animate; - ani.mode(timing).time(time).move(x, y); - } else if (animate.type === 'rotate') { - const ani = this.ensureAnimate(execution); - const { angle, time, timing } = animate; - ani.mode(timing).time(time).rotate(angle); - } else { - const ani = this.ensureAnimate(execution); - const { scale, time, timing } = animate; - ani.mode(timing).time(time).scale(scale); - } - } - - private ensureAnimate(execution: CameraAnimationExecution) { - if (execution.animation) return execution.animation; - const ani = new Animation(); - execution.animation = ani; - return ani; - } - - private ensureOperation(operation: CameraOperation) { - if (!this.animateMap.has(operation)) { - const data: CameraAnimationExecution = { - data: [], - animation: new Animation() - }; - this.animateMap.set(operation, data); - return data; - } else { - return this.animateMap.get(operation)!; - } - } - - /** - * 添加一个平移动画 - * @param operation 摄像机操作对象 - * @param x 目标横坐标 - * @param y 目标纵坐标 - * @param time 动画时长 - * @param start 动画开始时间 - * @param timing 动画的缓动函数 - */ - translate( - operation: ICameraTranslate, - x: number, - y: number, - time: number, - start: number, - timing: TimingFn - ) { - const exe = this.ensureOperation(operation); - const data: TranslateAnimation = { - type: 'translate', - timing, - x: x * 32, - y: y * 32, - time, - start - }; - exe.data.push(data); - } - - /** - * 添加一个旋转动画 - * @param operation 摄像机操作 - * @param angle 目标旋转弧度,单位弧度 - * @param time 动画时长 - * @param start 动画开始时间 - * @param timing 动画的缓动函数 - */ - rotate( - operation: ICameraRotate, - angle: number, - time: number, - start: number, - timing: TimingFn - ) { - const exe = this.ensureOperation(operation); - const data: RotateAnimation = { - type: 'rotate', - timing, - angle, - time, - start - }; - exe.data.push(data); - } - - /** - * 添加一个缩放动画 - * @param operation 摄像机操作 - * @param scale 目标缩放倍率 - * @param time 动画时长 - * @param start 动画开始时间 - * @param timing 动画的缓动函数 - */ - scale( - operation: ICameraScale, - scale: number, - time: number, - start: number, - timing: TimingFn - ) { - const exe = this.ensureOperation(operation); - const data: ScaleAnimation = { - type: 'scale', - timing, - scale, - time, - start - }; - exe.data.push(data); - } - - /** - * 开始执行这个动画 - */ - start() { - if (this.started) return; - this.startTime = Date.now(); - this.started = true; - let endTime = 0; - this.animateMap.forEach((exe, ope) => { - const data = exe.data; - data.sort((a, b) => a.start - b.start); - const end = data.at(-1); - if (!end) return; - const t = end.start + end.time; - if (t > endTime) endTime = t; - const cam = this.camera; - - if (ope.type === 'translate') { - cam.applyTranslateAnimation(ope, exe.animation, t + 100); - } else if (ope.type === 'rotate') { - cam.applyRotateAnimation(ope, exe.animation, t + 100); - } else { - cam.applyScaleAnimation(ope, exe.animation, t + 100); - } - }); - this.endTime = endTime + this.startTime; - this.tick(); - } - - destroy() { - this.camera.binded.removeTicker(this.delegation); - this.camera.stopAllAnimates(); - this.animateMap.forEach(v => { - v.animation.ticker.destroy(); - }); - this.animateMap.clear(); - } -} diff --git a/packages-user/client-modules/src/render/elements/damage.ts b/packages-user/client-modules/src/render/elements/damage.ts deleted file mode 100644 index 5a65d87..0000000 --- a/packages-user/client-modules/src/render/elements/damage.ts +++ /dev/null @@ -1,543 +0,0 @@ -import { - ERenderItemEvent, - RenderItem, - MotaOffscreenCanvas2D, - Transform, - transformCanvas -} from '@motajs/render'; -import { logger } from '@motajs/common'; -import EventEmitter from 'eventemitter3'; -import { isNil } from 'lodash-es'; -import { IDamageEnemy, IEnemyCollection, MapDamage } from '@motajs/types'; -import { BlockCacher, ICanvasCacheItem, CanvasCacheItem } from './block'; -import { - ILayerGroupRenderExtends, - LayerGroupFloorBinder, - LayerGroup, - Layer, - calNeedRenderOf -} from './layer'; -import { MAP_BLOCK_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared'; - -/** - * 根据伤害大小获取颜色 - * @param damage 伤害大小 - */ -export function getDamageColor(damage: number): string { - if (typeof damage !== 'number') return '#f00'; - if (damage === 0) return '#2f2'; - if (damage < 0) return '#7f7'; - if (damage < core.status.hero.hp / 3) return '#fff'; - if (damage < (core.status.hero.hp * 2) / 3) return '#ff4'; - if (damage < core.status.hero.hp) return '#f93'; - return '#f22'; -} - -interface EFloorDamageEvent { - update: [floor: FloorIds]; -} - -export class FloorDamageExtends - extends EventEmitter - implements ILayerGroupRenderExtends -{ - id: string = 'floor-damage'; - - floorBinder!: LayerGroupFloorBinder; - group!: LayerGroup; - sprite!: Damage; - - /** - * 立刻刷新伤害渲染 - */ - update(floor: FloorIds) { - if (!this.sprite || !floor) return; - const map = core.status.maps[floor]; - this.sprite.setMapSize(map.width, map.height); - const { ensureFloorDamage } = Mota.require('@user/data-state'); - ensureFloorDamage(floor); - const enemy = core.status.maps[floor].enemy; - - this.sprite.updateCollection(enemy); - this.emit('update', floor); - } - - /** - * 创建显伤层 - */ - private create() { - if (this.sprite) return; - const sprite = new Damage(); - sprite.setZIndex(80); - this.group.appendChild(sprite); - this.sprite = sprite; - } - - private onUpdate = (floor: FloorIds) => { - if (!this.floorBinder.bindThisFloor) { - const { ensureFloorDamage } = Mota.require('@user/data-state'); - ensureFloorDamage(floor); - core.status.maps[floor].enemy.calRealAttribute(); - } - this.update(floor); - }; - - // private onSetBlock = (x: number, y: number, floor: FloorIds) => { - // this.sprite.enemy?.once('extract', () => { - // if (floor !== this.sprite.enemy?.floorId) return; - // this.sprite.updateBlocks(); - // }); - // if (!this.floorBinder.bindThisFloor) { - // this.sprite.enemy?.extract(); - // } - // }; - - /** - * 进行楼层更新监听 - */ - private listen() { - this.floorBinder.on('update', this.onUpdate); - // this.floorBinder.on('setBlock', this.onSetBlock); - } - - awake(group: LayerGroup): void { - const ex = group.getExtends('floor-binder'); - if (ex instanceof LayerGroupFloorBinder) { - this.floorBinder = ex; - this.group = group; - this.create(); - this.listen(); - } else { - logger.warn(17); - group.removeExtends('floor-damage'); - } - } - - onDestroy(_group: LayerGroup): void { - this.floorBinder.off('update', this.onUpdate); - // this.floorBinder.off('setBlock', this.onSetBlock); - } -} - -export interface DamageRenderable { - x: number; - y: number; - align: CanvasTextAlign; - baseline: CanvasTextBaseline; - text: string; - color: CanvasStyle; - font?: string; - stroke?: CanvasStyle; - strokeWidth?: number; -} - -export interface EDamageEvent extends ERenderItemEvent { - setMapSize: [width: number, height: number]; - beforeDamageRender: [need: Set, transform: Transform]; - updateBlocks: [blocks: Set]; - dirtyUpdate: [block: number]; -} - -export class Damage extends RenderItem { - mapWidth: number = 0; - mapHeight: number = 0; - - block: BlockCacher; - /** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */ - renderable: Map> = new Map(); - - /** 当前渲染怪物列表 */ - enemy?: IEnemyCollection; - /** 每个分块中包含的怪物集合 */ - blockData: Map> = new Map(); - /** 单元格大小 */ - cellSize: number = 32; - - /** 默认伤害字体 */ - font: string = '300 9px Verdana'; - /** 默认描边样式,当伤害文字不存在描边属性时会使用此属性 */ - strokeStyle: CanvasStyle = '#000'; - /** 默认描边宽度 */ - strokeWidth: number = 2; - - /** 要懒更新的所有分块 */ - private dirtyBlocks: Set = new Set(); - - constructor() { - super('absolute', false, true); - - this.block = new BlockCacher(0, 0, MAP_BLOCK_WIDTH, 1); - this.type = 'absolute'; - this.size(MAP_WIDTH, MAP_HEIGHT); - this.setHD(true); - this.setAntiAliasing(true); - } - - protected render( - canvas: MotaOffscreenCanvas2D, - transform: Transform - ): void { - this.renderDamage(canvas, transform); - } - - private onExtract = () => { - if (this.enemy) this.updateCollection(this.enemy); - }; - - /** - * 设置地图大小,后面应紧跟更新怪物列表 - */ - setMapSize(width: number, height: number) { - this.mapWidth = width; - this.mapHeight = height; - this.enemy = void 0; - this.blockData.clear(); - this.renderable.clear(); - this.block.size(width, height); - - // 预留blockData - const w = this.block.blockData.width; - const h = this.block.blockData.height; - const num = w * h; - for (let i = 0; i < num; i++) { - this.blockData.set(i, new Map()); - this.renderable.set(i, new Set()); - this.dirtyBlocks.add(i); - } - - this.emit('setMapSize', width, height); - } - - /** - * 设置每个图块的大小 - */ - setCellSize(size: number) { - this.cellSize = size; - this.update(); - } - - /** - * 更新怪物列表。更新后,{@link Damage.enemy} 会丢失原来的怪物列表引用,换为传入的列表引用 - * @param enemy 怪物列表 - */ - updateCollection(enemy: IEnemyCollection) { - if (this.enemy !== enemy) { - this.enemy?.off('calculated', this.onExtract); - enemy.on('calculated', this.onExtract); - } - this.enemy = enemy; - this.blockData.forEach(v => v.clear()); - this.renderable.forEach(v => v.clear()); - this.block.clearAllCache(); - const w = this.block.blockData.width; - const h = this.block.blockData.height; - const num = w * h; - for (let i = 0; i < num; i++) { - this.dirtyBlocks.add(i); - } - - enemy.list.forEach(v => { - if (isNil(v.x) || isNil(v.y)) return; - const index = this.block.getIndexByLoc(v.x, v.y); - this.blockData.get(index)?.set(v.y * this.mapWidth + v.x, v); - }); - // this.updateBlocks(); - - this.update(this); - } - - /** - * 更新指定矩形区域内的渲染信息 - * @param x 左上角横坐标 - * @param y 左上角纵坐标 - * @param width 宽度 - * @param height 高度 - */ - updateRenderable(x: number, y: number, width: number, height: number) { - this.updateBlocks(this.block.updateElementArea(x, y, width, height)); - } - - /** - * 更新指定分块 - * @param blocks 要更新的分块集合 - * @param map 是否更新地图伤害 - */ - updateBlocks(blocks?: Set) { - if (blocks) { - blocks.forEach(v => this.dirtyBlocks.add(v)); - this.emit('updateBlocks', blocks); - } else { - this.blockData.forEach((_v, i) => { - this.dirtyBlocks.add(i); - }); - this.emit('updateBlocks', new Set(this.blockData.keys())); - } - this.update(this); - } - - /** - * 更新指定位置的怪物信息 - */ - updateEnemyOn(x: number, y: number) { - const enemy = this.enemy?.get(x, y); - const block = this.block.getIndexByLoc(x, y); - const data = this.blockData.get(block); - const index = x + y * this.mapWidth; - if (!data) return; - if (!enemy) { - data.delete(index); - } else { - data.set(index, enemy); - } - - this.update(this); - - // 渲染懒更新,优化性能表现 - this.dirtyBlocks.add(block); - } - - /** - * 更新单个分块 - * @param block 更新的分块 - * @param map 是否更新地图伤害 - */ - private updateBlock(block: number, map: boolean = true) { - const data = this.blockData.get(block); - if (!data) return; - - this.block.clearCache(block, 1); - const renderable = this.renderable.get(block)!; - - renderable.clear(); - data.forEach(v => this.extract(v, renderable)); - if (map) this.extractMapDamage(block, renderable); - } - - /** - * 将怪物解析为renderable的伤害 - * @param enemy 怪物 - * @param block 怪物所属分块 - */ - private extract(enemy: IDamageEnemy, block: Set) { - if (enemy.progress !== 4) return; - const x = enemy.x!; - const y = enemy.y!; - const { damage } = enemy.calDamage(); - const cri = enemy.calCritical(1)[0]?.atkDelta ?? Infinity; - - const dam1: DamageRenderable = { - align: 'left', - baseline: 'alphabetic', - text: isFinite(damage) ? core.formatBigNumber(damage, true) : '???', - color: getDamageColor(damage), - x: x * this.cellSize + 1, - y: y * this.cellSize + this.cellSize - 1 - }; - const dam2: DamageRenderable = { - align: 'left', - baseline: 'alphabetic', - text: isFinite(cri) ? core.formatBigNumber(cri, true) : '?', - color: '#fff', - x: x * this.cellSize + 1, - y: y * this.cellSize + this.cellSize - 11 - }; - block.add(dam1).add(dam2); - } - - /** - * 解析指定分块的地图伤害 - * @param block 分块索引 - */ - private extractMapDamage(block: number, renderable: Set) { - if (!this.enemy) return; - const damage = this.enemy.mapDamage; - const [sx, sy, ex, ey] = this.block.getRectOfIndex(block); - for (let x = sx; x < ex; x++) { - for (let y = sy; y < ey; y++) { - const loc = `${x},${y}`; - const dam = damage[loc]; - if (!dam) continue; - this.pushMapDamage(x, y, renderable, dam); - } - } - } - - /** - * 解析所有地图伤害 - */ - private extractAllMapDamage() { - // todo: 测试性能,这样真的会更快吗?或许能更好的优化?或者是根本不需要这个函数? - if (!this.enemy) return; - for (const [loc, enemy] of Object.entries(this.enemy.mapDamage)) { - const [sx, sy] = loc.split(','); - const x = Number(sx); - const y = Number(sy); - const block = this.renderable.get(this.block.getIndexByLoc(x, y))!; - this.pushMapDamage(x, y, block, enemy); - } - } - - private pushMapDamage( - x: number, - y: number, - block: Set, - dam: MapDamage - ) { - // todo: 这个应当可以自定义,通过地图伤害注册实现 - let text = ''; - const color = '#fa3'; - const font = '300 9px Verdana'; - if (dam.damage > 0) { - text = core.formatBigNumber(dam.damage, true); - } else if (dam.ambush) { - text = `!`; - } else if (dam.repulse) { - text = '阻'; - } - - const mapDam: DamageRenderable = { - align: 'center', - baseline: 'middle', - text, - color, - font, - x: x * this.cellSize + this.cellSize / 2, - y: y * this.cellSize + this.cellSize / 2 - }; - block.add(mapDam); - } - - /** - * 计算需要渲染哪些块 - */ - calNeedRender(transform: Transform) { - if (this.parent instanceof LayerGroup) { - // 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化 - return this.parent.cacheNeedRender(transform, this.block); - } else if (this.parent instanceof Layer) { - // 如果是地图的子元素,直接调用Layer的计算函数 - return this.parent.calNeedRender(transform); - } else { - return calNeedRenderOf(transform, this.cellSize, this.block); - } - } - - /** - * 渲染伤害层 - * @param transform 变换矩阵 - */ - renderDamage(canvas: MotaOffscreenCanvas2D, transform: Transform) { - // console.time('damage'); - const { ctx } = canvas; - ctx.save(); - transformCanvas(canvas, transform); - - const render = this.calNeedRender(transform); - const block = this.block; - const cell = this.cellSize; - const size = cell * block.blockSize; - - this.emit('beforeDamageRender', render, transform); - - render.forEach(v => { - const [x, y] = block.getBlockXYByIndex(v); - const bx = x * block.blockSize; - const by = y * block.blockSize; - const px = bx * cell; - const py = by * cell; - - // todo: 是否真的需要缓存 - // 检查有没有缓存 - const cache = block.cache.get(v); - if (cache && cache.symbol === cache.canvas.symbol) { - ctx.drawImage(cache.canvas.canvas, px, py, size, size); - return; - } - - if (this.dirtyBlocks.has(v)) { - this.updateBlock(v, true); - } - this.emit('dirtyUpdate', v); - - // 否则依次渲染并写入缓存 - const temp = block.cache.get(v)?.canvas ?? this.requireCanvas(); - temp.clear(); - temp.setHD(true); - temp.setAntiAliasing(true); - temp.size(size, size); - const { ctx: ct } = temp; - - ct.translate(-px, -py); - ct.lineJoin = 'round'; - ct.lineCap = 'round'; - - const render = this.renderable.get(v); - - render?.forEach(v => { - if (!v) return; - ct.fillStyle = v.color; - ct.textAlign = v.align; - ct.textBaseline = v.baseline; - ct.font = v.font ?? this.font; - ct.strokeStyle = v.stroke ?? this.strokeStyle; - ct.lineWidth = v.strokeWidth ?? this.strokeWidth; - - ct.strokeText(v.text, v.x, v.y); - ct.fillText(v.text, v.x, v.y); - }); - - ctx.drawImage(temp.canvas, px, py, size, size); - block.cache.set(v, new CanvasCacheItem(temp, temp.symbol, this)); - }); - ctx.restore(); - // console.timeEnd('damage'); - } - - protected handleProps( - key: string, - _prevValue: any, - nextValue: any - ): boolean { - switch (key) { - case 'mapWidth': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setMapSize(nextValue, this.mapHeight); - return true; - case 'mapHeight': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setMapSize(this.mapWidth, nextValue); - return true; - case 'cellSize': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setCellSize(nextValue); - return true; - case 'enemy': - if (!this.assertType(nextValue, 'object', key)) return false; - this.updateCollection(nextValue); - return true; - case 'font': - if (!this.assertType(nextValue, 'string', key)) return false; - this.font = nextValue; - this.update(); - return true; - case 'strokeStyle': - this.strokeStyle = nextValue; - this.update(); - return true; - case 'strokeWidth': - if (!this.assertType(nextValue, 'number', key)) return false; - this.strokeWidth = nextValue; - this.update(); - return true; - } - return false; - } - - destroy(): void { - super.destroy(); - this.block.destroy(); - this.enemy?.off('extract', this.onExtract); - } -} - -// const adapter = new RenderAdapter('damage'); diff --git a/packages-user/client-modules/src/render/elements/frame.ts b/packages-user/client-modules/src/render/elements/frame.ts deleted file mode 100644 index 2be9b6a..0000000 --- a/packages-user/client-modules/src/render/elements/frame.ts +++ /dev/null @@ -1,54 +0,0 @@ -import EventEmitter from 'eventemitter3'; -import { RenderItem } from '@motajs/render'; - -export interface IAnimateFrame { - updateFrameAnimate(frame: number, time: number): void; -} - -interface RenderEvent { - animateFrame: [frame: number, time: number]; -} - -class RenderEmits extends EventEmitter { - private framer: Set = new Set(); - - /** - * 添加一个可更新帧动画的对象 - */ - addFramer(framer: IAnimateFrame) { - this.framer.add(framer); - } - - /** - * 移除一个可更新帧动画的对象 - */ - removeFramer(framer: IAnimateFrame) { - this.framer.delete(framer); - } - - /** - * 更新所有帧动画 - * @param frame 帧数 - * @param time 帧动画时刻 - */ - emitAnimateFrame(frame: number, time: number) { - this.framer.forEach(v => v.updateFrameAnimate(frame, time)); - this.emit('animateFrame', frame, time); - } -} - -export const renderEmits = new RenderEmits(); - -export function createFrame() { - Mota.require('@user/data-base').hook.once('reset', () => { - let lastTime = 0; - RenderItem.ticker.add(time => { - if (!core.isPlaying()) return; - if (time - lastTime > core.values.animateSpeed) { - RenderItem.animatedFrame++; - lastTime = time; - renderEmits.emitAnimateFrame(RenderItem.animatedFrame, time); - } - }); - }); -} diff --git a/packages-user/client-modules/src/render/elements/hero.ts b/packages-user/client-modules/src/render/elements/hero.ts deleted file mode 100644 index 908a09a..0000000 --- a/packages-user/client-modules/src/render/elements/hero.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { SizedCanvasImageSource, RenderAdapter } from '@motajs/render'; -import { logger } from '@motajs/common'; -import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer'; -import EventEmitter from 'eventemitter3'; -import { texture } from './cache'; -import { TimingFn } from 'mutate-animate'; -import { isNil } from 'lodash-es'; - -// type HeroMovingStatus = 'stop' | 'moving' | 'moving-as'; -// export const enum HeroMovingStatus {} - -interface HeroRenderEvent { - stepEnd: []; - moveTick: [x: number, y: number]; - append: [renderable: LayerMovingRenderable[]]; -} - -export class HeroRenderer - extends EventEmitter - implements ILayerRenderExtends -{ - id: string = 'floor-hero'; - - /** 勇士的图片资源 */ - image?: SizedCanvasImageSource; - cellWidth?: number; - cellHeight?: number; - - /** 勇士的渲染信息 */ - renderable?: LayerMovingRenderable; - layer!: Layer; - - /** 当前移动帧数 */ - movingFrame: number = 0; - /** 是否正在移动 */ - moving: boolean = false; - /** 是否正在播放动画,与移动分开以实现原地踏步 */ - animate: boolean = false; - - /** 勇士移动速度 */ - speed: number = 100; - /** 勇士移动的真正速度,经过录像修正 */ - realSpeed: number = 100; - - /** 当前的移动方向 */ - moveDir: Dir2 = 'down'; - /** 当前移动的勇士显示方向 */ - showDir: Dir = 'down'; - /** 帧动画是否反向播放,例如后退时就应该设为true */ - animateReverse: boolean = false; - - /** 勇士移动定时器id */ - private moveId: number = -1; - /** 上一次帧数切换的时间 */ - private lastFrameTime: number = 0; - /** 上一步走到格子上的时间 */ - private lastStepTime: number = 0; - /** 执行当前步移动的Promise */ - private moveDetached?: Promise; - /** endMove的Promise */ - private moveEnding?: Promise; - /** - * 这一步的移动方向,与{@link moveDir}不同的是,在这一步走完之前,它都不会变, - * 当这一步走完之后,才会将其设置为{@link moveDir}的值 - */ - stepDir: Dir2 = 'down'; - /** 每步的格子增量 */ - private stepDelta: Loc = { x: 0, y: 1 }; - - /** - * 设置勇士所用的图片资源 - * @param image 图片资源 - */ - setImage(image: SizedCanvasImageSource) { - this.image = image; - this.split(); - this.resetRenderable(true); - this.layer.requestUpdateMoving(); - } - - /** - * 设置移动的移动速度 - * @param speed 移动速度 - */ - setMoveSpeed(speed: number) { - this.speed = speed; - this.fixMoveSpeed(); - } - - /** - * 分割勇士图像,生成renderable信息 - */ - split() { - this.cellWidth = this.image!.width / 4; - this.cellHeight = this.image!.height / 4; - this.generateRenderable(); - } - - /** - * 生成渲染信息 - */ - generateRenderable() { - if (!this.image) return; - this.renderable = { - image: this.image, - frame: 4, - x: core.status.hero.loc.x, - y: core.status.hero.loc.y, - zIndex: core.status.hero.loc.y, - autotile: false, - bigImage: true, - render: this.getRenderFromDir(this.showDir), - animate: 0, - alpha: 1 - }; - } - - /** - * 根据方向获取勇士的裁切信息 - * @param dir 方向 - */ - getRenderFromDir(dir: Dir): [number, number, number, number][] { - if (!this.cellWidth || !this.cellHeight) return []; - const index = texture.characterDirection[dir]; - const y = index * this.cellHeight; - const res: [number, number, number, number][] = [0, 1, 2, 3].map(v => { - return [v * this.cellWidth!, y, this.cellWidth!, this.cellHeight!]; - }); - if (this.animateReverse) return res.reverse(); - else return res; - } - - /** - * 开始勇士帧动画 - */ - startAnimate() { - this.animate = true; - this.lastFrameTime = Date.now(); - } - - /** - * 结束勇士帧动画 - */ - endAnimate() { - this.animate = false; - this.resetRenderable(false); - } - - /** - * 设置帧动画是否反向播放 - * @param reverse 帧动画是否反向播放 - */ - setAnimateReversed(reverse: boolean) { - this.animateReverse = reverse; - this.resetRenderable(true); - } - - /** - * 设置勇士的动画显示朝向 - * @param dir 显示朝向 - */ - setAnimateDir(dir: Dir) { - if (dir !== this.showDir) { - this.showDir = dir; - this.resetRenderable(true); - } - } - - /** - * 重置renderable状态,恢复至第一帧状态 - * @param getInfo 是否重新获取渲染信息 - */ - resetRenderable(getInfo: boolean) { - this.movingFrame = 0; - - if (this.renderable) { - this.renderable.animate = 0; - if (getInfo) { - this.renderable.render = this.getRenderFromDir(this.showDir); - } - } - this.layer.update(this.layer); - } - - /** - * 勇士帧动画定时器 - */ - private animateTick(time: number) { - if (!this.animate) return; - if (time - this.lastFrameTime > this.speed) { - this.lastFrameTime = time; - this.movingFrame++; - this.movingFrame %= 4; - if (this.renderable) this.renderable.animate = this.movingFrame; - } - this.layer.update(this.layer); - } - - /** - * 勇士移动定时器 - */ - private moveTick(time: number) { - if (!this.renderable) return; - - if (this.moving) { - const progress = (time - this.lastStepTime) / this.realSpeed; - - const { x: dx, y: dy } = this.stepDelta; - const { x, y } = core.status.hero.loc; - if (progress >= 1) { - this.renderable.x = x + dx; - this.renderable.y = y + dy; - this.fixMoveSpeed(); - this.emit('stepEnd'); - } else { - const rx = dx * progress + x; - const ry = dy * progress + y; - this.renderable.x = rx; - this.renderable.y = ry; - } - this.layer.update(this.layer); - } - this.emit('moveTick', this.renderable.x, this.renderable.y); - } - - /** - * 进行下一步的移动准备,设置移动信息 - */ - private step() { - this.stepDir = this.moveDir; - this.lastStepTime = Date.now(); - this.stepDelta = core.utils.scan2[this.stepDir]; - this.turn(this.stepDir); - } - - private fixMoveSpeed() { - if (!core.isReplaying()) { - this.realSpeed = this.speed; - } else { - const replay = core.status.replay.speed; - this.realSpeed = replay === 24 ? 1 : this.speed / replay; - } - } - - /** - * 准备开始移动 - */ - readyMove() { - this.moving = true; - this.fixMoveSpeed(); - } - - /** - * 移动勇士 - */ - move(dir: Dir2): Promise { - if (!this.moving) { - logger.error(12); - return Promise.reject( - 'Cannot moving hero while hero not in moving!' - ); - } - - this.moveDir = dir; - if (this.moveDetached) { - return this.moveDetached; - } else { - this.step(); - this.moveDetached = new Promise(res => { - this.once('stepEnd', () => { - this.moveDetached = void 0; - res(); - }); - }); - return this.moveDetached; - } - } - - /** - * 结束勇士的移动过程 - */ - endMove(): Promise { - if (!this.moving) return Promise.resolve(); - if (this.moveEnding) return this.moveEnding; - else { - const promise = new Promise(resolve => { - this.once('stepEnd', () => { - this.moveEnding = void 0; - this.moving = false; - const { x, y } = core.status.hero.loc; - this.setHeroLoc(x, y); - this.render(); - resolve(); - }); - }); - return (this.moveEnding = promise); - } - } - - /** - * 勇士转向,不填表示顺时针转一个方向 - * @param dir 移动方向 - */ - turn(dir?: Dir2): void { - if (!dir) { - const index = texture.characterTurn2.indexOf(this.stepDir); - if (index === -1) { - const length = texture.characterTurn.length; - const index = texture.characterTurn.indexOf( - this.stepDir as Dir - ); - const next = texture.characterTurn[index % length]; - return this.turn(next); - } else { - return this.turn(texture.characterTurn[index]); - } - } - this.moveDir = dir; - this.stepDir = dir; - - if (!this.renderable) return; - this.renderable.render = this.getRenderFromDir(this.showDir); - this.layer.update(this.layer); - } - - /** - * 设置勇士的坐标 - * @param x 横坐标 - * @param y 纵坐标 - */ - setHeroLoc(x?: number, y?: number) { - if (!this.renderable) return; - if (!isNil(x)) { - this.renderable.x = x; - } - if (!isNil(y)) { - this.renderable.y = y; - } - this.emit('moveTick', this.renderable.x, this.renderable.y); - this.layer.update(this.layer); - } - - /** - * 按照指定函数移动勇士 - * @param x 目标横坐标 - * @param y 目标纵坐标 - * @param time 移动时间 - * @param fn 移动函数,传入一个完成度(范围0-1),返回一个三元素数组,表示横纵格子坐标,可以是小数。 - * 第三个元素表示图块纵深,一般图块的纵深就是其纵坐标,当地图上有大怪物时,此举可以辅助渲染, - * 否则可能会导致移动过程中与大怪物的层级关系不正确,比如全在大怪物身后。注意不建议频繁改动这个值, - * 因为此举会导致层级的重新排序,降低渲染性能。 - */ - moveAs(x: number, y: number, time: number, fn: TimingFn<3>): Promise { - if (!this.moving) return Promise.resolve(); - if (!this.renderable) return Promise.resolve(); - const nowZIndex = fn(0)[2]; - const startTime = Date.now(); - return new Promise(res => { - this.layer.delegateTicker( - () => { - if (!this.renderable) return; - const now = Date.now(); - const progress = (now - startTime) / time; - const [nx, ny, nz] = fn(progress); - this.renderable.x = nx; - this.renderable.y = ny; - this.renderable.zIndex = nz; - if (nz !== nowZIndex) { - this.layer.sortMovingRenderable(); - } - this.emit('moveTick', this.renderable.x, this.renderable.y); - this.layer.update(this.layer); - }, - time, - () => { - this.moving = false; - if (!this.renderable) return res(); - this.renderable.x = x; - this.renderable.y = y; - this.emit('moveTick', this.renderable.x, this.renderable.y); - this.layer.update(this.layer); - res(); - } - ); - }); - } - - /** - * 渲染勇士 - */ - render() { - if (!this.renderable) return; - if (!this.animate) { - this.renderable.animate = 0; - } else { - this.renderable.animate = this.movingFrame; - } - this.layer.update(this.layer); - } - - awake(layer: Layer): void { - this.layer = layer; - adapter.add(this); - this.moveId = layer.delegateTicker(() => { - const time = Date.now(); - this.animateTick(time); - this.moveTick(time); - }); - if (core.status.hero) { - const image = core.status.hero.image; - this.setImage(core.material.images.images[image]); - } - } - - onDestroy(layer: Layer): void { - adapter.remove(this); - layer.removeTicker(this.moveId); - } - - onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void { - if (this.renderable) { - renderable.push(this.renderable); - this.emit('append', renderable); - } - } -} - -const adapter = new RenderAdapter('hero-adapter'); -adapter.receive('readyMove', item => { - item.readyMove(); - return Promise.resolve(); -}); -adapter.receive('move', (item, dir: Dir) => { - return item.move(dir); -}); -adapter.receive('endMove', item => { - return item.endMove(); -}); -adapter.receive( - 'moveAs', - (item, x: number, y: number, time: number, fn: TimingFn<3>) => { - return item.moveAs(x, y, time, fn); - } -); -adapter.receive('setHeroLoc', (item, x?: number, y?: number) => { - item.setHeroLoc(x, y); - return Promise.resolve(); -}); -adapter.receive('turn', (item, dir: Dir2) => { - item.turn(dir); - return Promise.resolve(); -}); - -// 同步适配函数,这些函数用于同步设置信息等 -adapter.receiveSync('setImage', (item, image: SizedCanvasImageSource) => { - item.setImage(image); -}); -adapter.receiveSync('setMoveSpeed', (item, speed: number) => { - item.setMoveSpeed(speed); -}); -adapter.receiveSync('setAnimateReversed', (item, reverse: boolean) => { - item.setAnimateReversed(reverse); -}); -adapter.receiveSync('startAnimate', item => { - item.startAnimate(); -}); -adapter.receiveSync('endAnimate', item => { - item.endAnimate(); -}); -adapter.receiveSync('setAnimateDir', (item, dir: Dir) => { - item.setAnimateDir(dir); -}); - -// 不分同步fallback,用于适配现在的样板,之后会删除 -adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => { - item.setHeroLoc(x, y); -}); -adapter.receiveSync('turn', (item, dir: Dir2) => { - item.turn(dir); -}); diff --git a/packages-user/client-modules/src/render/elements/index.ts b/packages-user/client-modules/src/render/elements/index.ts index 5aaaa1b..780b33a 100644 --- a/packages-user/client-modules/src/render/elements/index.ts +++ b/packages-user/client-modules/src/render/elements/index.ts @@ -1,115 +1,47 @@ -import { standardElementNoCache, tagMap } from '@motajs/render-vue'; -import { createCache } from './cache'; -import { createFrame } from './frame'; -import { createLayer, Layer, LayerGroup } from './layer'; -import { createViewport } from './viewport'; -import { Icon, Winskin } from './misc'; -import { Animate } from './animate'; -import { createItemDetail } from './itemDetail'; import { logger } from '@motajs/common'; -import { MapExtensionManager, MapRender, MapRenderer } from '../map'; -import { state } from '@user/data-state'; -import { materials } from '@user/client-base'; +import { MapRenderItem } from '../map'; +import { mainRenderer, tagManager } from '../renderer'; +import { createCache } from './cache'; +import { Icon, Winskin } from './misc'; export function createElements() { createCache(); - createFrame(); - createLayer(); - createViewport(); - createItemDetail(); // ----- 注册标签 + mainRenderer.registerElement('icon', Icon); + mainRenderer.registerElement('winskin', Winskin); + mainRenderer.registerElement('map-render', MapRenderItem); - tagMap.register('winskin', (_0, _1, props) => { - if (!props) - return new Winskin(core.material.images.images['winskin.png']); - else { - const { - image = core.material.images.images['winskin.png'], - type = 'static' - } = props; - return new Winskin(image, type); - } - }); - tagMap.register('layer', (_0, _1, props) => { - if (!props) return new Layer(); - else { - const { ex } = props; - const l = new Layer(); - - if (ex) { - (ex as any[]).forEach(v => { - l.extends(v); - }); - } - - return l; - } - }); - tagMap.register('layer-group', (_0, _1, props) => { - if (!props) return new LayerGroup(); - else { - const { ex, layers } = props; - const l = new LayerGroup(); - - if (ex) { - (ex as any[]).forEach(v => { - l.extends(v); - }); - } - if (layers) { - (layers as any[]).forEach(v => { - l.addLayer(v); - }); - } - - return l; - } - }); - tagMap.register('animation', (_0, _1, _props) => { - return new Animate(); - }); - tagMap.register('icon', standardElementNoCache(Icon)); - tagMap.register('map-render', (_0, _1, props) => { + tagManager.registerTag( + 'icon', + tagManager.createStandardElement(false, Icon) + ); + tagManager.registerTag( + 'winskin', + tagManager.createStandardElement(false, Winskin) + ); + tagManager.registerTag('map-render', props => { if (!props) { - logger.error(42, 'layerState, renderer, extenstion'); - const renderer = new MapRenderer(materials, state.layer); - const manager = new MapExtensionManager(renderer); - return new MapRender(state.layer, renderer, manager); + logger.error(42, 'layerState'); + throw new Error(`Lack of map-render property.`); } const { layerState, renderer, extension } = props; if (!layerState) { logger.error(42, 'layerState'); - const renderer = new MapRenderer(materials, state.layer); - const manager = new MapExtensionManager(renderer); - return new MapRender(state.layer, renderer, manager); + throw new Error(`Lack of map-render property.`); } if (!renderer) { logger.error(42, 'renderer'); - const renderer = new MapRenderer(materials, state.layer); - const manager = new MapExtensionManager(renderer); - return new MapRender(state.layer, renderer, manager); + throw new Error(`Lack of map-render property.`); } if (!extension) { logger.error(42, 'extension'); - const renderer = new MapRenderer(materials, state.layer); - const manager = new MapExtensionManager(renderer); - return new MapRender(state.layer, renderer, manager); + throw new Error(`Lack of map-render property.`); } - return new MapRender(layerState, renderer, extension); + return new MapRenderItem(layerState, renderer, extension); }); } -export * from './animate'; -export * from './block'; export * from './cache'; -export * from './camera'; -export * from './damage'; -export * from './frame'; -export * from './hero'; -export * from './itemDetail'; -export * from './layer'; export * from './misc'; export * from './props'; -export * from './utils'; -export * from './viewport'; diff --git a/packages-user/client-modules/src/render/elements/itemDetail.ts b/packages-user/client-modules/src/render/elements/itemDetail.ts deleted file mode 100644 index 726e297..0000000 --- a/packages-user/client-modules/src/render/elements/itemDetail.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { logger } from '@motajs/common'; -import { mainSetting } from '@motajs/legacy-ui'; -import { hook } from '@user/data-base'; -import { ItemState } from '@user/data-state'; -import { Damage, DamageRenderable, FloorDamageExtends } from './damage'; -import { - ILayerGroupRenderExtends, - LayerGroup, - LayerGroupFloorBinder -} from './layer'; - -interface ItemDetailData { - x: number; - y: number; - diff: Record; -} - -interface ItemData { - id: AllIdsOf<'items'>; - x: number; - y: number; -} - -export function createItemDetail() { - hook.on('setBlock', (x, y, _floorId, block) => { - FloorItemDetail.listened.forEach(v => { - v.setBlock(block, x, y); - }); - }); -} - -export class FloorItemDetail implements ILayerGroupRenderExtends { - id: string = 'item-detail'; - - group!: LayerGroup; - floorBinder!: LayerGroupFloorBinder; - damage!: FloorDamageExtends; - sprite!: Damage; - - /** 每个分块中包含的物品信息 */ - blockData: Map> = new Map(); - - /** 需要更新的分块 */ - private dirtyBlock: Set = new Set(); - /** 道具详细信息 */ - private detailData: Map> = new Map(); - - static detailColor: Record = { - atk: '#FF7A7A', - atkper: '#FF7A7A', - def: '#00E6F1', - defper: '#00E6F1', - mdef: '#6EFF83', - mdefper: '#6EFF83', - hp: '#A4FF00', - hpmax: '#F9FF00', - hpmaxper: '#F9FF00', - mana: '#c66', - manaper: '#c66' - }; - - static listened: Set = new Set(); - - private onBeforeDamageRender = (block: number) => { - if (!mainSetting.getValue('screen.itemDetail')) return; - if (this.dirtyBlock.has(block)) { - this.sprite.block.clearCache(block, 1); - } - this.render(block); - }; - - private onUpdateMapSize = (width: number, height: number) => { - this.updateMapSize(width, height); - }; - - private onUpdate = () => { - this.updateItems(); - }; - - private onUpdateBlocks = (blocks: Set) => { - blocks.forEach(v => { - this.dirtyBlock.add(v); - }); - }; - - private listen() { - this.sprite.on('dirtyUpdate', this.onBeforeDamageRender); - this.sprite.on('setMapSize', this.onUpdateMapSize); - this.sprite.on('updateBlocks', this.onUpdateBlocks); - this.damage.on('update', this.onUpdate); - } - - /** - * 更新地图大小 - */ - updateMapSize(width: number, height: number) { - this.blockData.clear(); - - // 预留blockData - this.sprite.block.size(width, height); - const data = this.sprite.block.blockData; - const num = data.width * data.height; - for (let i = 0; i <= num; i++) { - this.blockData.set(i, new Map()); - this.detailData.set(i, new Map()); - this.dirtyBlock.add(i); - } - } - - /** - * 更新全地图的物品,并进行分块存储 - */ - updateItems() { - const floor = this.floorBinder.getFloor(); - if (!floor) return; - core.extractBlocks(floor); - - core.status.maps[floor].blocks.forEach(v => { - if (v.event.cls !== 'items' || v.disable) return; - const id = v.event.id as AllIdsOf<'items'>; - const item = core.material.items[id]; - if (item.cls === 'constants' || item.cls === 'tools') return; - const x = v.x; - const y = v.y; - const block = this.sprite.block.getIndexByLoc(x, y); - const index = x + y * this.sprite.mapWidth; - const blockData = this.blockData.get(block); - blockData?.set(index, { x, y, id }); - }); - } - - /** - * 设置图块 - * @param block 图块数字 - * @param x 横坐标 - * @param y 纵坐标 - */ - setBlock(block: AllNumbers, x: number, y: number) { - const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e; - const index = this.sprite.block.getIndexByLoc(x, y); - const itemIndex = x + y * this.sprite.mapWidth; - const blockData = this.blockData.get(index); - this.dirtyBlock.add(index); - if (block === 0) { - blockData?.delete(itemIndex); - return; - } - const cls = map[block].cls; - if (cls !== 'items') { - blockData?.delete(itemIndex); - return; - } - const id = map[block].id; - blockData?.set(itemIndex, { x, y, id }); - } - - /** - * 计算指定分块中的物品信息 - * @param block 要计算的分块 - */ - calAllItems(block: Set) { - const enable = mainSetting.getValue('screen.itemDetail'); - if (!core.status.thisMap || !enable) return; - if (this.dirtyBlock.size === 0 || block.size === 0) return; - - let diff: Record = {}; - const before = core.status.hero; - const hero = structuredClone(core.status.hero); - const handler: ProxyHandler = { - set(target, key, v) { - diff[key] = v - (target[key] || 0); - if (!diff[key]) diff[key] = void 0; - return true; - } - }; - core.status.hero = new Proxy(hero, handler); - - core.setFlag('__statistics__', true); - this.dirtyBlock.forEach(v => { - const data = this.blockData.get(v); - const detail = this.detailData.get(v); - detail?.clear(); - if (!data) return; - data.forEach(v => { - const { id, x, y } = v; - const index = x + y * this.sprite.mapWidth; - diff = {}; - - const item = core.material.items[id]; - if (item.cls === 'equips') { - // 装备也显示 - const diff: Record = { - ...(item.equip.value ?? {}) - }; - const per = item.equip.percentage ?? {}; - for (const name of Object.keys(per)) { - const n = name as SelectKey; - diff[name + 'per'] = per[n].toString() + '%'; - } - detail?.set(index, { x, y, diff }); - return; - } - - ItemState.item(id)?.itemEffectFn?.(); - detail?.set(index, { x, y, diff }); - }); - }); - core.status.hero = before; - window.hero = before; - window.flags = before.flags; - } - - /** - * 计算并渲染指定格子里面的物品 - * @param block 需要渲染的格子 - */ - render(block: number) { - if (this.dirtyBlock.has(block)) { - this.calAllItems(new Set([block])); - } - const data = this.detailData; - this.dirtyBlock.delete(block); - const info = data.get(block); - if (!info) return; - info.forEach(({ x, y, diff }) => { - let n = 0; - for (const [key, value] of Object.entries(diff)) { - if (!value) continue; - const color = FloorItemDetail.detailColor[key] ?? '#fff'; - const text = core.formatBigNumber(value, 4); - const renderable: DamageRenderable = { - x: x * this.sprite.cellSize + 2, - y: y * this.sprite.cellSize + 31 - n * 10, - text, - color, - align: 'left', - baseline: 'alphabetic' - }; - this.sprite.renderable.get(block)?.add(renderable); - n++; - } - }); - } - - awake(group: LayerGroup): void { - this.group = group; - - const binder = group.getExtends('floor-binder'); - const damage = group.getExtends('floor-damage'); - if ( - binder instanceof LayerGroupFloorBinder && - damage instanceof FloorDamageExtends - ) { - this.floorBinder = binder; - this.damage = damage; - this.sprite = damage.sprite; - this.listen(); - FloorItemDetail.listened.add(this); - } else { - logger.warn(1001); - group.removeExtends('item-detail'); - } - } - - onDestroy(_group: LayerGroup): void { - this.sprite.off('beforeDamageRender', this.onBeforeDamageRender); - this.sprite.off('setMapSize', this.onUpdateMapSize); - FloorItemDetail.listened.delete(this); - } -} diff --git a/packages-user/client-modules/src/render/elements/layer.ts b/packages-user/client-modules/src/render/elements/layer.ts deleted file mode 100644 index d316330..0000000 --- a/packages-user/client-modules/src/render/elements/layer.ts +++ /dev/null @@ -1,1959 +0,0 @@ -import { - Container, - EContainerEvent, - MotaOffscreenCanvas2D, - Sprite, - RenderItem, - Transform, - RenderAdapter -} from '@motajs/render'; -import { logger } from '@motajs/common'; -import { sleep, TimingFn } from 'mutate-animate'; -import { RenderableData, texture } from './cache'; -import { BlockCacher, CanvasCacheItem, ICanvasCacheItem } from './block'; -import { IAnimateFrame, renderEmits } from './frame'; -import { EventEmitter } from 'eventemitter3'; -import { - MAP_BLOCK_HEIGHT, - MAP_BLOCK_WIDTH, - MAP_HEIGHT, - MAP_WIDTH -} from '../shared'; - -export interface ILayerGroupRenderExtends { - /** 拓展的唯一标识符 */ - readonly id: string; - - /** - * 当拓展被激活时执行的函数(一般就是拓展加载至目标LayerGroup实例时立刻执行) - * @param group 目标LayerGroup实例 - */ - awake?(group: LayerGroup): void; - - /** - * 当一个Layer层级被添加时执行的函数 - * @param group 目标LayerGroup实例 - * @param layer 添加的Layer层实例 - */ - onLayerAdd?(group: LayerGroup, layer: Layer): void; - - /** - * 当一个Layer层级被移除时执行的函数 - * @param group 目标LayerGroup实例 - * @param layer 移除的Layer层实例 - */ - onLayerRemove?(group: LayerGroup, layer: Layer): void; - - /** - * 当一个Layer层级从显示到隐藏的状态切换时执行的函数 - * @param group 目标LayerGroup实例 - * @param layer 隐藏的Layer层实例 - */ - onLayerHide?(group: LayerGroup, layer: Layer): void; - - /** - * 当一个Layer层级从隐藏到显示状态切换时执行的函数 - * @param group 目标LayerGroup实例 - * @param layer 显示的Layer层实例 - */ - onLayerShow?(group: LayerGroup, layer: Layer): void; - - /** - * 当执行 {@link LayerGroup.emptyLayer} 时执行的函数,即清空所有挂载的Layer时执行的函数 - * @param group 目标LayerGroup实例 - */ - onEmptyLayer?(group: LayerGroup): void; - - /** - * 当帧动画更新时执行的函数,例如从第一帧变成第二帧时 - * @param group 目标LayerGroup实例 - * @param frame 当前帧数 - */ - onFrameUpdate?(group: LayerGroup, frame: number): void; - - /** - * 在渲染之前执行的函数 - * @param group 目标LayerGroup实例 - */ - onBeforeRender?(group: LayerGroup): void; - - /** - * 在渲染之后执行的函数 - * @param group 目标LayerGroup实例 - */ - onAfterRender?(group: LayerGroup): void; - - /** - * 当拓展被取消挂载时执行的函数(LayerGroup被销毁,拓展被移除等) - * @param group 目标LayerGroup实例 - */ - onDestroy?(group: LayerGroup): void; -} - -export type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2'; - -const layerZIndex: Record = { - bg: 10, - bg2: 20, - event: 30, - fg: 40, - fg2: 50 -}; - -export interface ELayerGroupEvent extends EContainerEvent {} - -export class LayerGroup - extends Container - implements IAnimateFrame -{ - /** 地图组列表 */ - // static list: Set = new Set(); - - cellSize: number = 32; - blockSize: number = MAP_BLOCK_WIDTH; - - /** 当前楼层 */ - floorId?: FloorIds; - /** 是否绑定了当前层 */ - bindThisFloor: boolean = false; - /** 伤害显示层 */ - // damage?: Damage; - /** 地图显示层 */ - layers: Map = new Map(); - - /** 这个地图组的摄像机 */ - camera: Transform = new Transform(); - - private needRender?: Set; - readonly extend: Map = new Map(); - - constructor() { - super('static', true); - - this.setHD(true); - this.setAntiAliasing(false); - this.size(MAP_WIDTH, MAP_HEIGHT); - - this.on('afterRender', () => { - this.releaseNeedRender(); - }); - - renderEmits.addFramer(this); - - const binder = new LayerGroupFloorBinder(); - this.extends(binder); - binder.bindThis(); - } - - protected render(canvas: MotaOffscreenCanvas2D): void { - this.sortedChildren.forEach(v => { - if (v.hidden) return; - v.renderContent(canvas, this.camera); - }); - } - - /** - * 添加渲染拓展,可以将渲染拓展理解为一类插件,通过指定的函数在对应时刻执行一些函数, - * 来达到执行自己想要的功能的效果。例如样板自带的勇士渲染、伤害渲染等都由此实现。 - * 具体能干什么参考 {@link ILayerGroupRenderExtends} - * @param ex 渲染拓展对象 - */ - extends(ex: ILayerGroupRenderExtends) { - this.extend.set(ex.id, ex); - ex.awake?.(this); - } - - /** - * 移除一个渲染拓展 - * @param id 要移除的拓展 - */ - removeExtends(id: string) { - const ex = this.extend.get(id); - if (!ex) return; - this.extend.delete(id); - ex.onDestroy?.(this); - } - - /** - * 获取一个已装载的拓展 - * @param id 拓展id - */ - getExtends(id: string) { - return this.extend.get(id); - } - - /** - * 设置渲染分块大小 - * @param size 分块大小 - */ - setBlockSize(size: number) { - this.blockSize = size; - this.layers.forEach(v => { - v.block.setBlockSize(size); - }); - } - - /** - * 设置每个图块的大小 - * @param size 每个图块的大小 - */ - setCellSize(size: number) { - this.cellSize = size; - this.layers.forEach(v => { - v.setCellSize(size); - }); - } - - /** - * 清空所有层 - */ - emptyLayer() { - this.removeChild(...this.layers.values()); - this.layers.forEach(v => v.destroy()); - this.layers.clear(); - - for (const ex of this.extend.values()) { - ex.onEmptyLayer?.(this); - } - } - - /** - * 添加显示层 - * @param layer 显示层 - */ - addLayer(layer: FloorLayer | Layer) { - if (typeof layer === 'string') { - const l = new Layer(); - l.layer = layer; - this.layers.set(layer, l); - l.setZIndex(layerZIndex[layer]); - this.appendChild(l); - - for (const ex of this.extend.values()) { - ex.onLayerAdd?.(this, l); - } - - return l; - } else { - if (layer.layer) { - this.layers.set(layer.layer, layer); - for (const ex of this.extend.values()) { - ex.onLayerAdd?.(this, layer); - } - } - return layer; - } - } - - /** - * 移除指定层 - * @param layer 要移除的层,可以是Layer实例,也可以是字符串 - */ - removeLayer(layer: FloorLayer | Layer) { - let ins: Layer | undefined; - if (typeof layer === 'string') { - const la = this.layers.get(layer); - if (!la) return; - this.removeChild(la); - this.layers.delete(layer); - la.destroy(); - ins = la; - } else { - const arr = [...this.layers]; - const la = arr.find(v => v[1] === layer)?.[0]; - if (la && this.layers.delete(la)) { - this.removeChild(layer); - layer.destroy(); - ins = layer; - } - } - if (ins) { - for (const ex of this.extend.values()) { - ex.onLayerRemove?.(this, ins); - } - } - } - - /** - * 获取一个地图层实例,例如获取背景层等 - * @param layer 地图层 - */ - getLayer(layer: FloorLayer) { - return this.layers.get(layer); - } - - /** - * 隐藏某个层 - * @param layer 要隐藏的层 - */ - hideLayer(layer: FloorLayer) { - const la = this.getLayer(layer); - if (!la) return; - la.hide(); - - for (const ex of this.extend.values()) { - ex.onLayerHide?.(this, la); - } - } - - /** - * 显示某个层 - * @param layer 要显示的层 - */ - showLayer(layer: FloorLayer) { - const la = this.getLayer(layer); - if (!la) return; - la.show(); - - for (const ex of this.extend.values()) { - ex.onLayerShow?.(this, la); - } - } - - /** - * 缓存计算应该渲染的块 - * @param transform 变换矩阵 - * @param blockData 分块信息 - */ - cacheNeedRender(transform: Transform, block: BlockCacher) { - return ( - this.needRender ?? - (this.needRender = calNeedRenderOf(transform, this.cellSize, block)) - ); - } - - /** - * 释放应该渲染块缓存 - */ - releaseNeedRender() { - this.needRender = void 0; - } - - /** - * 更新动画帧 - */ - updateFrameAnimate() { - this.update(this); - - for (const ex of this.extend.values()) { - ex.onFrameUpdate?.(this, RenderItem.animatedFrame % 4); - } - } - - protected handleProps( - key: string, - _prevValue: any, - nextValue: any - ): boolean { - switch (key) { - case 'cellSize': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setCellSize(nextValue); - return true; - case 'blockSize': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setBlockSize(nextValue); - return true; - case 'floorId': { - if (!this.assertType(nextValue, 'number', key)) return false; - const binder = this.getExtends('floor-binder'); - if (binder instanceof LayerGroupFloorBinder) { - binder.bindFloor(nextValue); - } - return true; - } - case 'camera': - if (!this.assertType(nextValue, Transform, key)) return false; - this.camera = nextValue; - return true; - } - return false; - } - - destroy(): void { - for (const ex of this.extend.values()) { - ex.onDestroy?.(this); - } - super.destroy(); - renderEmits.removeFramer(this); - } -} - -export function calNeedRenderOf( - transform: Transform, - cell: number, - block: BlockCacher -): Set { - const w = MAP_BLOCK_WIDTH * cell; - const h = MAP_BLOCK_HEIGHT * cell; - const size = block.blockSize; - const width = block.blockData.width; - - // -1是因为宽度是core._PX_,从0开始的话,末尾索引就是core._PX_ - 1 - const [px1, py1] = Transform.untransformed(transform, 0, 0); - const [px2, py2] = Transform.untransformed(transform, w - 1, 0); - const [px3, py3] = Transform.untransformed(transform, w - 1, h - 1); - const [px4, py4] = Transform.untransformed(transform, 0, h - 1); - - const maxX = block.width * cell - 1; - const maxY = block.height * cell - 1; - - const res: Set = new Set(); - // 实际上不太可能一次性渲染非常多的图块,因此不需要非常细致地算出所有的格点,整体包含即可 - // 因此直接算其最小外接矩形即可 - const left = Math.max(0, Math.min(px1, px2, px3, px4)); - const right = Math.min(maxX, Math.max(px1, px2, px3, px4)); - const top = Math.max(0, Math.min(py1, py2, py3, py4)); - const bottom = Math.max(maxY, Math.max(py1, py2, py3, py4)); - - const blockLeft = Math.floor(left / cell / size); - const blockRight = Math.floor(right / cell / size); - const blockTop = Math.floor(top / cell / size); - const blockBottom = Math.floor(bottom / cell / size); - - for (let y = blockTop; y <= blockBottom; y++) { - for (let x = blockLeft; x <= blockRight; x++) { - res.add(x + y * width); - } - } - - return res; -} - -export interface ILayerRenderExtends { - /** 拓展的唯一标识符 */ - readonly id: string; - - /** - * 当拓展被激活时执行的函数(一般就是拓展加载至目标Layer实例时立刻执行) - * @param layer 目标Layer实例 - */ - awake?(layer: Layer): void; - - /** - * 当楼层的背景图块被设置时执行的函数 - * @param layer 目标Layer实例 - * @param background 设置为的背景图块数字 - */ - onBackgroundSet?(layer: Layer, background: AllNumbers): void; - - /** - * 当背景图块图片被生成时执行的函数 - * @param layer 目标Layer实例 - * @param images 生成出的背景图块的单个分块图像,数组是因为背景图块可能是多帧图块 - */ - onBackgroundGenerated?(layer: Layer, images: MotaOffscreenCanvas2D[]): void; - - /** - * 当修改渲染数据时执行的函数,参见 {@link Layer.putRenderData} - * @param layer 目标Layer实例 - * @param data 扁平化的数据信息 - * @param width 数据宽度 - * @param x 数据左上角横坐标 - * @param y 数据左上角纵坐标 - * @param calAutotile 是否重新计算自动元件的连接情况 - */ - onDataPut?( - layer: Layer, - data: number[], - width: number, - x: number, - y: number, - calAutotile: boolean - ): void; - - /** - * 当更新某个区域内的大怪物renderable信息时执行的函数 - * @param layer 目标Layer实例 - * @param x 左上角横坐标 - * @param y 左上角纵坐标 - * @param width 区域宽度 - * @param height 区域高度 - * @param images 最终的大怪物renderable信息,等同于 {@link Layer.bigImages} - */ - onBigImagesUpdate?( - layer: Layer, - x: number, - y: number, - width: number, - height: number, - images: Map - ): void; - - /** - * 当计算完成区域内自动元件连接信息时执行的函数 - * @param layer 目标Layer实例 - * @param x 左上角横坐标 - * @param y 左上角纵坐标 - * @param width 区域宽度 - * @param height 区域高度 - * @param autotiles 计算出的自动元件连接信息,等同于 {@link Layer.autotiles} - */ - onAutotilesCaled?( - layer: Layer, - x: number, - y: number, - width: number, - height: number, - autotiles: Record - ): void; - - /** - * 当地图大小修改时执行的函数 - * @param layer 目标Layer实例 - * @param width 地图宽度 - * @param height 地图高度 - */ - onMapResize?(layer: Layer, width: number, height: number): void; - - /** - * 当更新指定区域的分块缓存时执行的函数 - * @param layer 目标Layer实例 - * @param blocks 更新区域内包含的分块索引 - * @param x 区域的图格左上角横坐标 - * @param y 区域的图格右上角横坐标 - * @param width 区域的图格宽度 - * @param height 区域的图格高度 - */ - onBlocksUpdate?( - layer: Layer, - blocks: Set, - x: number, - y: number, - width: number, - height: number - ): void; - - /** - * 当更新移动层的渲染信息是执行的函数 - * @param layer 目标Layer实例 - * @param renderable 移动层的渲染信息(包含大怪物),未排序 - */ - onMovingUpdate?(layer: Layer, renderable: LayerMovingRenderable[]): void; - - /** - * 在地图渲染之前执行的函数 - * @param layer 目标Layer实例 - * @param transform 渲染的变换矩阵 - * @param need 需要渲染的分块信息 - */ - onBeforeRender?( - layer: Layer, - transform: Transform, - need: Set - ): void; - - /** - * 在地图渲染之后执行的函数 - * @param layer 目标Layer实例 - * @param transform 渲染的变换矩阵 - * @param need 需要渲染的分块信息 - */ - onAfterRender?(layer: Layer, transform: Transform, need: Set): void; - - /** - * 当拓展被取消挂载时执行的函数(Layer被销毁,拓展被移除等) - * @param layer 目标Layer实例 - */ - onDestroy?(layer: Layer): void; -} - -export interface LayerMovingRenderable extends RenderableData { - zIndex: number; - x: number; - y: number; - alpha: number; -} - -export interface ELayerEvent extends EContainerEvent {} - -export class Layer extends Container { - // 一些会用到的常量 - static readonly FRAME_0 = 1; - static readonly FRAME_1 = 2; - static readonly FRAME_2 = 4; - static readonly FRAME_3 = 8; - static readonly FRAME_ALL = 15; - - /** 静态层,包含除大怪物及正在移动的内容外的内容 */ - protected staticMap = this.requireCanvas(true, false); - /** 移动层,包含大怪物及正在移动的内容 */ - protected movingMap = this.requireCanvas(true, false); - /** 背景图层 */ - protected backMap = this.requireCanvas(true, false); - - /** 最终渲染至的Sprite */ - main: Sprite = new Sprite('absolute', false, true); - - /** 渲染的层 */ - layer?: FloorLayer; - // todo: renderable分块存储,优化循环绘制性能 - /** 渲染数据 */ - renderData: number[] = []; - /** 自动元件的连接信息,键表示图块在渲染数据中的索引,值表示连接信息,是个8位二进制 */ - autotiles: Record = {}; - /** 楼层宽度 */ - mapWidth: number = 0; - /** 楼层高度 */ - mapHeight: number = 0; - /** 每个图块的大小 */ - cellSize: number = 32; - - /** 背景图块 */ - background: AllNumbers = 0; - /** 背景图块画布 */ - backImage: MotaOffscreenCanvas2D[] = []; - /** 背景贴图 */ - floorImage: FloorAnimate[] = []; - - /** 分块信息 */ - block: BlockCacher = new BlockCacher( - 0, - 0, - MAP_BLOCK_WIDTH, - 4 - ); - - /** 大怪物渲染信息 */ - bigImages: Map = new Map(); - // todo: 是否需要桶排? - /** 移动层的渲染信息 */ - movingRenderable: LayerMovingRenderable[] = []; - /** 下一次渲染时是否需要更新移动层的渲染信息 */ - needUpdateMoving: boolean = false; - - private extend: Map = new Map(); - /** 正在移动的图块的渲染信息 */ - moving: Set = new Set(); - - constructor() { - super('absolute', false, true); - - // this.setHD(false); - this.setAntiAliasing(false); - this.size(MAP_WIDTH, MAP_HEIGHT); - - this.staticMap.setHD(false); - this.staticMap.setAntiAliasing(false); - this.staticMap.size(MAP_WIDTH, MAP_HEIGHT); - this.movingMap.setHD(false); - this.movingMap.setAntiAliasing(false); - this.movingMap.size(MAP_WIDTH, MAP_HEIGHT); - this.backMap.setHD(false); - this.backMap.setAntiAliasing(false); - this.backMap.size(MAP_WIDTH, MAP_HEIGHT); - this.main.setAntiAliasing(false); - this.main.setHD(false); - this.main.size(MAP_WIDTH, MAP_HEIGHT); - - this.appendChild(this.main); - this.main.setRenderFn((canvas, transform) => { - const { ctx } = canvas; - const { width, height } = canvas; - const need = this.calNeedRender(transform); - this.renderMap(transform, need); - ctx.drawImage(this.backMap.canvas, 0, 0, width, height); - ctx.drawImage(this.staticMap.canvas, 0, 0, width, height); - ctx.drawImage(this.movingMap.canvas, 0, 0, width, height); - }); - - this.extends(new LayerFloorBinder()); - layerAdapter.add(this); - } - - /** - * 添加渲染拓展,可以将渲染拓展理解为一类插件,通过指定的函数在对应时刻执行一些函数, - * 来达到执行自己想要的功能的效果。例如样板自带的勇士渲染、伤害渲染等都由此实现。 - * 具体能干什么参考 {@link ILayerRenderExtends} - * @param ex 渲染拓展对象 - */ - extends(ex: ILayerRenderExtends) { - this.extend.set(ex.id, ex); - ex.awake?.(this); - } - - /** - * 移除一个渲染拓展 - * @param id 要移除的拓展 - */ - removeExtends(id: string) { - const ex = this.extend.get(id); - if (!ex) return; - this.extend.delete(id); - ex.onDestroy?.(this); - } - - /** - * 获取一个已装载的拓展 - * @param id 拓展id - */ - getExtends(id: string) { - return this.extend.get(id); - } - - /** - * 判断一个点是否在地图范围内 - * @param x 横坐标 - * @param y 纵坐标 - */ - isPointOutside(x: number, y: number) { - return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight; - } - - /** - * 判断一个矩形是否完全在地图之外 - * @param x 矩形左上角横坐标 - * @param y 矩形左上角纵坐标 - * @param width 矩形长度 - * @param height 矩形高度 - */ - isRectOutside(x: number, y: number, width: number, height: number) { - return ( - x >= this.mapWidth || - y >= this.mapHeight || - x + width < 0 || - y + height < 0 - ); - } - - /** - * 判断一个矩形是否完全在地图之内 - * @param x 矩形左上角横坐标 - * @param y 矩形左上角纵坐标 - * @param width 矩形长度 - * @param height 矩形高度 - */ - containsRect(x: number, y: number, width: number, height: number) { - return ( - x + width <= this.mapWidth && - y + height <= this.mapHeight && - x >= 0 && - y >= 0 - ); - } - - /** - * 设置每个图块的大小 - * @param size 每个图块的大小 - */ - setCellSize(size: number) { - this.cellSize = size; - this.update(); - } - - /** - * 设置楼层贴图 - */ - setFloorImage(image: FloorAnimate[]) { - this.floorImage = image; - this.update(); - } - - /** - * 设置背景图块 - * @param background 背景图块 - */ - setBackground(background: AllNumbers) { - this.background = background; - this.generateBackground(); - - for (const ex of this.extend.values()) { - ex.onBackgroundSet?.(this, background); - } - } - - /** - * 将当前地图的背景图块绑定为一个地图的背景图块 - * @param floorId 楼层id - */ - bindBackground(floorId: FloorIds) { - const { defaultGround } = core.status.maps[floorId]; - if (defaultGround) { - this.setBackground(texture.idNumberMap[defaultGround]); - } - } - - /** - * 生成背景图块 - */ - generateBackground() { - const num = this.background; - - const data = texture.getRenderable(num); - this.backImage.forEach(v => this.deleteCanvas(v)); - this.backImage = []; - if (!data) return; - - const frame = data.frame; - const temp = this.requireCanvas(true, false); - temp.setHD(false); - temp.setAntiAliasing(false); - for (let i = 0; i < frame; i++) { - const canvas = this.requireCanvas(true, false); - const ctx = canvas.ctx; - const tempCtx = temp.ctx; - const [sx, sy, w, h] = data.render[i]; - canvas.setHD(false); - canvas.setAntiAliasing(false); - canvas.size(MAP_WIDTH, MAP_HEIGHT); - temp.size(w, h); - - const img = data.autotile ? data.image[0b11111111] : data.image; - tempCtx.drawImage(img, sx, sy, w, h, 0, 0, w, h); - const pattern = ctx.createPattern(temp.canvas, 'repeat'); - if (!pattern) continue; - ctx.fillStyle = pattern; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - this.backImage.push(canvas); - } - this.deleteCanvas(temp); - - for (const ex of this.extend.values()) { - ex.onBackgroundGenerated?.(this, this.backImage); - } - } - - /** - * 修改地图渲染数据,对于溢出的内容会进行裁剪 - * @param data 要渲染的地图数据 - * @param width 数据的宽度 - * @param x 第一个数据的横坐标,默认是0 - * @param y 第一个数据的纵坐标,默认是0 - */ - putRenderData( - data: number[], - width: number, - x: number = 0, - y: number = 0, - calAutotile: boolean = true - ) { - if (data.length % width !== 0) { - logger.warn(8); - data.push(...Array(width - (data.length % width)).fill(0)); - } - const height = Math.round(data.length / width); - if (!this.containsRect(x, y, width, height)) { - logger.warn(9); - if (this.isRectOutside(x, y, width, height)) return; - } - // 特判特殊情况-全地图更新 - if ( - x === 0 && - y === 0 && - width === this.mapWidth && - height === this.mapHeight - ) { - // 为了不丢失引用,需要先清空,然后填充,不能直接赋值 - this.renderData.splice(0); - this.renderData.push(...data); - } else if (data.length === 1) { - // 特判单个图块的情况 - const index = x + y * this.mapWidth; - this.renderData[index] = data[0]; - } else { - // 限定更新区域 - const startX = Math.max(0, x); - const startY = Math.max(0, y); - const endX = Math.min(this.mapWidth, width); - const endY = Math.min(this.mapHeight, height); - for (let nx = startX; nx < endX; nx++) { - for (let ny = startY; ny < endY; ny++) { - // dx和dy表示数据在传入的data中的位置 - const dx = nx - x; - const dy = ny - y; - const index = dx + dy * width; - const indexData = nx + nx * this.mapWidth; - this.renderData[indexData] = data[index]; - } - } - } - // todo: 异步优化,到下一帧再更新 - if (calAutotile) this.calAutotiles(x, y, width, height); - this.updateBlocks(x, y, width, height); - this.updateBigImages(x, y, width, height); - - for (const ex of this.extend.values()) { - ex.onDataPut?.(this, data, width, x, y, calAutotile); - } - } - - /** - * 更新大怪物的渲染信息 - */ - updateBigImages(x: number, y: number, width: number, height: number) { - const ex = x + width; - const ey = y + height; - const w = this.mapWidth; - const data = this.renderData; - - for (let nx = x; nx < ex; nx++) { - for (let ny = y; ny < ey; ny++) { - const index = ny * w + nx; - this.bigImages.delete(index); - const num = data[index]; - const renderable = texture.getRenderable(num); - if (!renderable || !renderable.bigImage) continue; - this.bigImages.set(index, { - ...renderable, - x: nx, - y: ny, - zIndex: ny, - alpha: 1 - }); - } - } - - this.needUpdateMoving = true; - - for (const ex of this.extend.values()) { - ex.onBigImagesUpdate?.(this, x, y, width, height, this.bigImages); - } - } - - /** - * 计算自动元件的连接信息(会丢失autotiles属性的引用) - */ - calAutotiles(x: number, y: number, width: number, height: number) { - const sx = x - 1; - const sy = y - 1; - const ex = x + width + 1; - const ey = y + height + 1; - const data = this.renderData; - const tile = texture.autotile; - const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e; - - const w = this.mapWidth; - const h = this.mapHeight; - - // todo: 如何定向优化? - // this.autotiles = {}; - - /** - * 检查连接信息 - * @param id 比较对象的id(就是正在检查周围的那个自动元件,九宫格中心的) - * @param index1 比较对象 - * @param index2 被比较对象 - * @param replace1 被比较对象相对比较对象应该处理的位数 - * @param replace2 比较对象相对被比较对象应该处理的位数 - */ - const check = ( - x1: number, - y1: number, - x2: number, - y2: number, - replace1: number, - _replace2: number - ) => { - const index1 = x1 + y1 * w; - const index2 = x2 + y2 * w; - this.autotiles[index1] ??= 0; - this.autotiles[index2] ??= 0; - // 与地图边缘,视为连接 - if (x2 < 0 || y2 < 0 || x2 >= w || y2 >= h) { - this.autotiles[index1] |= replace1; - return; - } - const num1 = data[index1] as AllNumbersOf<'autotile'>; // 这个一定是自动元件 - const num2 = data[index2] as AllNumbersOf<'autotile'>; - // 对于额外连接的情况 - const autoConn = texture.getAutotileConnections(num1); - if (autoConn?.has(num2)) { - this.autotiles[index1] |= replace1; - return; - } - const info = map[num2 as Exclude]; - if (!info || info.cls !== 'autotile') { - // 被比较对象不是自动元件 - this.autotiles[index1] &= ~replace1; - } else { - const parent2 = tile[num2].parent; - if (num2 === num1) { - // 二者一样,视为连接 - this.autotiles[index1] |= replace1; - } else if (parent2?.has(num1)) { - // 被比较对象是比较对象的父元件,那么比较对象视为连接 - this.autotiles[index1] |= replace1; - } else { - // 上述条件都不满足,那么不连接 - this.autotiles[index1] &= ~replace1; - } - } - }; - - for (let nx = sx; nx < ex; nx++) { - if (nx >= w || nx < 0) continue; - for (let ny = sy; ny < ey; ny++) { - if (ny >= h || ny < 0) continue; - const index = nx + ny * w; - const num = data[index]; - // 特判空气墙与空图块 - if (num === 0 || num === 17 || num >= 10000) continue; - - const info = map[num as Exclude]; - const { cls } = info; - if (cls !== 'autotile') continue; - - // 太地狱了这个,看看就好 - // 左上 左 左下 - check(nx, ny, nx - 1, ny - 1, 0b10000000, 0b00001000); - check(nx, ny, nx - 1, ny, 0b00000001, 0b00010000); - check(nx, ny, nx - 1, ny + 1, 0b00000010, 0b00100000); - // 上 右上 - check(nx, ny, nx, ny - 1, 0b01000000, 0b00000100); - check(nx, ny, nx + 1, ny - 1, 0b00100000, 0b00000010); - // 右 右下 下 - check(nx, ny, nx + 1, ny, 0b00010000, 0b00000001); - check(nx, ny, nx + 1, ny + 1, 0b00001000, 0b10000000); - check(nx, ny, nx, ny + 1, 0b00000100, 0b01000000); - } - } - - for (const ex of this.extend.values()) { - ex.onAutotilesCaled?.(this, x, y, width, height, this.autotiles); - } - } - - /** - * 设置地图大小,会清空渲染数据(且丢失引用),因此后面应当紧跟 putRenderData,以保证渲染正常进行 - * @param width 地图宽度 - * @param height 地图高度 - */ - setMapSize(width: number, height: number) { - this.mapWidth = width; - this.mapHeight = height; - this.renderData = Array(width * height).fill(0); - this.autotiles = {}; - this.block.size(width, height); - this.block.clearAllCache(); - this.bigImages.clear(); - this.moving.clear(); - - for (const ex of this.extend.values()) { - ex.onMapResize?.(this, width, height); - } - } - - /** - * 给定一个矩形,更新其包含的块信息,注意由于自动元件的存在,实际判定范围会大一圈 - * @param x 图格的左上角横坐标 - * @param y 图格的左上角纵坐标 - * @param width 横向有多少个图格 - * @param height 纵向有多少个图格 - */ - updateBlocks(x: number, y: number, width: number, height: number) { - const blocks = this.block.updateElementArea( - x, - y, - width, - height, - Layer.FRAME_ALL - ); - - this.update(this); - - for (const ex of this.extend.values()) { - ex.onBlocksUpdate?.(this, blocks, x, y, width, height); - } - } - - /** - * 计算在传入的变换矩阵下,应该渲染哪些内容 - * @param transform 变换矩阵 - */ - calNeedRender(transform: Transform): Set { - if (this.parent instanceof LayerGroup) { - // 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化 - return this.parent.cacheNeedRender(transform, this.block); - } else { - return calNeedRenderOf(transform, this.cellSize, this.block); - } - } - - /** - * 更新移动层的渲染信息 - */ - updateMovingRenderable() { - this.movingRenderable = []; - this.movingRenderable.push(...this.bigImages.values()); - this.movingRenderable.push(...this.moving); - - for (const ex of this.extend.values()) { - ex.onMovingUpdate?.(this, this.movingRenderable); - } - this.sortMovingRenderable(); - } - - /** - * 对移动层按照z坐标排序 - */ - sortMovingRenderable() { - this.movingRenderable.sort((a, b) => a.zIndex - b.zIndex); - } - - /** - * 在下一帧更新moving层 - */ - requestUpdateMoving() { - this.needUpdateMoving = true; - } - - /** - * 渲染当前地图 - */ - renderMap(transform: Transform, need: Set) { - this.staticMap.clear(); - this.movingMap.clear(); - this.backMap.clear(); - - if (this.needUpdateMoving) this.updateMovingRenderable(); - this.needUpdateMoving = false; - - for (const ex of this.extend.values()) { - ex.onBeforeRender?.(this, transform, need); - } - - this.renderBack(transform, need); - this.renderStatic(transform, need); - this.renderMoving(transform); - for (const ex of this.extend.values()) { - ex.onAfterRender?.(this, transform, need); - } - } - - /** - * 渲染背景图 - * @param transform 变换矩阵 - * @param need 需要渲染的块 - */ - protected renderBack(transform: Transform, need: Set) { - const cell = this.cellSize; - const frame = (RenderItem.animatedFrame % 4) + 1; - const blockSize = this.block.blockSize; - const { ctx } = this.backMap; - const { width } = this.block.blockData; - - const mat = transform.mat; - const [a, b, , c, d, , e, f] = mat; - ctx.setTransform(a, b, c, d, e, f); - - if (this.background !== 0) { - // 画背景图 - const length = this.backImage.length; - const img = this.backImage[frame % length]; - need.forEach(index => { - if (index >= this.block.area || index < 0) return; - const x = index % width; - const y = Math.floor(index / width); - const sx = x * blockSize; - const sy = y * blockSize; - ctx.drawImage( - img.canvas, - sx * cell, - sy * cell, - blockSize * cell, - blockSize * cell - ); - }); - } - - if (this.floorImage.length > 0) { - const images = core.material.images.images; - this.floorImage.forEach(v => { - if (v.disable) return; - const { x, y } = v; - ctx.fillRect(0, 0, MAP_WIDTH, MAP_HEIGHT); - ctx.drawImage(images[v.name], x, y); - }); - } - } - - /** - * 渲染静态层 - */ - protected renderStatic(transform: Transform, need: Set) { - const cell = this.cellSize; - const frame = RenderItem.animatedFrame % 4; - const { width } = this.block.blockData; - const blockSize = this.block.blockSize; - const { ctx } = this.staticMap; - - const [a, b, , c, d, , e, f] = transform.mat; - ctx.setTransform(a, b, c, d, e, f); - - const extend = this.getExtends('floor-binder') as LayerFloorBinder; - const floor = extend ? extend.getFloor() : void 0; - const map = - this.layer === 'event' && floor - ? core.status.mapBlockObjs[floor] - : void 0; - need.forEach(v => { - const x = v % width; - const y = Math.floor(v / width); - const sx = x * blockSize; - const sy = y * blockSize; - const index = v * 4 + frame; - - const cache = this.block.cache.get(index); - if (cache) { - ctx.drawImage( - cache.canvas.canvas, - sx * cell, - sy * cell, - blockSize * cell, - blockSize * cell - ); - return; - } - - const ex = Math.min(sx + blockSize, this.mapWidth); - const ey = Math.min(sy + blockSize, this.mapHeight); - - const temp = this.requireCanvas(true, false); - temp.setAntiAliasing(false); - temp.setHD(false); - temp.size(MAP_WIDTH, MAP_HEIGHT); - - // 先画到临时画布,用于缓存 - for (let nx = sx; nx < ex; nx++) { - for (let ny = sy; ny < ey; ny++) { - if (map) { - const indexLoc = `${nx},${ny}`; - const block = map[indexLoc as LocString]; - if (block?.disable) continue; - } - const blockIndex = nx + ny * this.mapWidth; - const num = this.renderData[blockIndex]; - if (num === 0 || num === 17) continue; - const data = texture.getRenderable(num); - if (!data || data.bigImage) continue; - const f = frame % data.frame; - const i = data.animate === -1 ? f : data.animate; - const [isx, isy, w, h] = data.render[i]; - const px = (nx - sx) * cell; - const py = (ny - sy) * cell; - const { image, autotile } = data; - if (!autotile) { - temp.ctx.drawImage(image, isx, isy, w, h, px, py, w, h); - } else { - const link = this.autotiles[blockIndex]; - const i = image[link]; - temp.ctx.drawImage(i, isx, isy, w, h, px, py, w, h); - } - } - } - ctx.drawImage( - temp.canvas, - sx * cell, - sy * cell, - blockSize * cell, - blockSize * cell - ); - this.block.cache.set( - index, - new CanvasCacheItem(temp, temp.symbol, this) - ); - }); - } - - /** - * 渲染移动/大怪物层 - */ - protected renderMoving(transform: Transform) { - const frame = RenderItem.animatedFrame; - const cell = this.cellSize; - const halfCell = cell / 2; - const { ctx } = this.movingMap; - - const mat = transform.mat; - const [a, b, , c, d, , e, f] = mat; - ctx.setTransform(a, b, c, d, e, f); - const max1 = 1 / Math.min(a, b, c, d) ** 2; - const max2 = Math.max(MAP_WIDTH, MAP_HEIGHT) * 2; - const r = (max1 * max2) ** 2; - - this.movingRenderable.forEach(v => { - const { x, y, image, render, animate, alpha } = v; - const ff = frame % v.frame; - const i = animate === -1 ? ff : animate; - const [sx, sy, w, h] = render[i]; - const px = x * cell - w / 2 + halfCell; - const py = y * cell - h + cell; - const ex = px + w; - const ey = py + h; - - if ( - (px + e) ** 2 > r || - (py + f) ** 2 > r || - (ex + e) ** 2 > r || - (ey + f) ** 2 > r - ) { - return; - } - - ctx.globalAlpha = alpha; - ctx.drawImage(image, sx, sy, w, h, px, py, w, h); - }); - } - - /** - * 对图块进行线性插值移动或瞬移\ - * 线性插值移动:就是匀速平移,可以斜向移动\ - * 瞬移:立刻移动到目标点 - * @param index 要移动的图块在渲染数据中的索引位置 - * @param type 线性插值移动或瞬移 - * @param x 目标点横坐标 - * @param y 目标点纵坐标 - * @param time 移动总时长,注意不是每格时长 - */ - move( - index: number, - type: 'linear' | 'swap', - x: number, - y: number, - time?: number - ): Promise { - const block = this.renderData[index]; - const fx = index % this.width; - const fy = Math.floor(index / this.width); - - if (type === 'swap' || time === 0) { - this.putRenderData([0], 1, fx, fy); - this.putRenderData([block], 1, x, y); - return Promise.resolve(); - } else { - if (!time) return Promise.reject(); - const dx = x - fx; - const dy = y - fy; - return this.moveAs( - index, - x, - y, - progress => { - return [dx * progress, dy * progress, Math.floor(dy + fy)]; - }, - time - ); - } - } - - /** - * 让图块按照一个函数进行移动 - * @param index 要移动的图块在渲染数据中的索引位置 - * @param x 目标位置横坐标 - * @param y 目标位置纵坐标 - * @param fn 移动函数,传入一个完成度(范围0-1),返回一个三元素数组,表示横纵格子坐标,可以是小数。 - * 第三个元素表示图块纵深,一般图块的纵深就是其纵坐标,当地图上有大怪物时,此举可以辅助渲染, - * 否则可能会导致移动过程中与大怪物的层级关系不正确,比如全在大怪物身后。注意不建议频繁改动这个值, - * 因为此举会导致层级的重新排序,降低渲染性能。 - * @param time 移动总时长 - * @param relative 是否是相对模式 - */ - moveAs( - index: number, - x: number, - y: number, - fn: TimingFn<3>, - time: number, - keep: boolean = false, - relative: boolean = true - ): Promise { - const block = this.renderData[index]; - const fx = index % this.mapWidth; - const fy = Math.floor(index / this.mapWidth); - const moving = Layer.getMovingRenderable(block, fx, fy); - if (!moving) return Promise.reject(); - - this.moving.add(moving); - - // 删除原始位置的图块 - this.putRenderData([0], 1, fx, fy); - - const nowZ = fy; - const startTime = Date.now(); - return new Promise(resolve => { - this.delegateTicker( - () => { - const now = Date.now(); - const progress = (now - startTime) / time; - const [nx, ny, nz] = fn(progress); - const tx = relative ? nx + fx : nx; - const ty = relative ? ny + fy : ny; - moving.x = tx; - moving.y = ty; - moving.zIndex = nz; - if (nz !== nowZ) { - this.movingRenderable.sort( - (a, b) => a.zIndex - b.zIndex - ); - } - this.update(this); - }, - time, - () => { - if (keep) this.putRenderData([block], 1, x, y); - this.moving.delete(moving); - resolve(); - } - ); - }); - } - - /** - * 移动一个可移动的renderable - * @param data 移动renderable - * @param x 起始横坐标,注意与`moveAs`的`x`区分 - * @param y 起始纵坐标,注意与`moveAs`的`y`区分 - * @param fn 移动函数 - * @param time 移动时间 - * @param relative 是否是相对模式,默认相对模式 - */ - moveRenderable( - data: LayerMovingRenderable, - x: number, - y: number, - fn: TimingFn<3>, - time: number, - relative: boolean = true - ) { - const nowZ = y; - const startTime = Date.now(); - return new Promise(resolve => { - this.delegateTicker( - () => { - const now = Date.now(); - const progress = (now - startTime) / time; - const [nx, ny, nz] = fn(progress); - const tx = relative ? nx + x : nx; - const ty = relative ? ny + y : ny; - data.x = tx; - data.y = ty; - data.zIndex = nz; - if (nz !== nowZ) { - this.movingRenderable.sort( - (a, b) => a.zIndex - b.zIndex - ); - } - this.update(this); - }, - time, - () => { - resolve(); - } - ); - }); - } - - protected handleProps( - key: string, - _prevValue: any, - nextValue: any - ): boolean { - switch (key) { - case 'layer': { - if (!this.assertType(nextValue, 'string', key)) return false; - const parent = this.parent; - if (parent instanceof LayerGroup) { - parent.removeLayer(this); - this.layer = nextValue; - parent.addLayer(this); - } else { - this.layer = nextValue; - } - this.update(); - return true; - } - case 'cellSize': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setCellSize(nextValue); - return true; - case 'mapWidth': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setMapSize(nextValue, this.mapHeight); - return true; - case 'mapHeight': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setMapSize(this.mapWidth, nextValue); - return true; - case 'background': - if (!this.assertType(nextValue, 'number', key)) return false; - this.setBackground(nextValue); - return true; - case 'floorImage': - if (!this.assertType(nextValue, Array, key)) return false; - this.setFloorImage(nextValue as FloorAnimate[]); - return true; - } - return false; - } - - private addToGroup(group: LayerGroup) { - if (this.layer) { - group.addLayer(this); - } - } - - private removeFromGroup(group: LayerGroup) { - if (this.layer) { - group.removeLayer(this); - } - } - - appendTo(parent: RenderItem): void { - super.appendTo(parent); - if (parent instanceof LayerGroup) { - this.addToGroup(parent); - } - } - - remove(): boolean { - if (this.parent instanceof LayerGroup) { - this.removeFromGroup(this.parent); - } - return super.remove(); - } - - destroy(): void { - for (const ex of this.extend.values()) { - ex.onDestroy?.(this); - } - super.destroy(); - this.block.destroy(); - this.main.destroy(); - layerAdapter.remove(this); - } - - /** - * 根据图块信息初始化移动信息 - * @param num 图块数字 - * @param x 横坐标 - * @param y 纵坐标 - */ - static getMovingRenderable(num: number, x: number, y: number) { - const renderable = texture.getRenderable(num); - if (!renderable) return null; - const image = renderable.autotile - ? renderable.image[0] - : renderable.image; - const moving: LayerMovingRenderable = { - x: x, - y: y, - zIndex: y, - image: image, - autotile: false, - frame: renderable.frame, - bigImage: renderable.bigImage, - animate: -1, - render: renderable.render, - alpha: 1 - }; - return moving; - } -} - -const layerAdapter = new RenderAdapter('layer'); - -export function createLayer() { - const { hook } = Mota.require('@user/data-base'); - - hook.on('setBlock', (x, y, floor, block) => { - const isNow = floor === core.status.floorId; - LayerGroupFloorBinder.activeBinder.forEach(v => { - if (floor === v.floor || (isNow && v.bindThisFloor)) { - v.setBlock('event', block, x, y); - } - }); - LayerFloorBinder.listenedBinder.forEach(v => { - if (v.layer.layer === 'event') { - if (v.floor === floor || (isNow && v.bindThisFloor)) { - v.setBlock(block, x, y); - } - } - }); - }); - hook.on('changingFloor', floor => { - // 潜在隐患:如果putRenderData改成异步,那么会变成两帧后才能真正刷新并渲染 - // 考虑到楼层转换一般不会同时执行很多次,因此这里改为立刻更新 - LayerGroupFloorBinder.activeBinder.forEach(v => { - if (v.bindThisFloor) v.updateBindData(); - v.emit('floorChange', floor); - }); - LayerFloorBinder.listenedBinder.forEach(v => { - if (v.bindThisFloor) v.updateBindData(); - }); - }); - hook.on('setBgFgBlock', (name, number, x, y, floor) => { - const isNow = floor === core.status.floorId; - LayerGroupFloorBinder.activeBinder.forEach(v => { - if (floor === v.floor || (isNow && v.bindThisFloor)) { - v.setBlock(name, number, x, y); - } - }); - LayerFloorBinder.listenedBinder.forEach(v => { - if (v.layer.layer === name) { - if (v.floor === floor || (isNow && v.bindThisFloor)) { - v.setBlock(number, x, y); - } - } - }); - }); -} - -interface LayerGroupBinderEvent { - update: [floor: FloorIds]; - setBlock: [x: number, y: number, floor: FloorIds, block: AllNumbers]; - floorChange: [floor: FloorIds]; -} - -/** - * 楼层绑定拓展,用于LayerGroup,将楼层数据传输到渲染系统。 - * 添加后,会自动在LayerGroup包含的子Layer上添加LayerFloorBinder拓展,用于后续处理。 - * 当移除这个拓展时,其附属的所有子拓展也会一并被移除。 - */ -export class LayerGroupFloorBinder - extends EventEmitter - implements ILayerGroupRenderExtends -{ - id: string = 'floor-binder'; - - bindThisFloor: boolean = true; - floor?: FloorIds; - group!: LayerGroup; - - /** 附属的子LayerFloorBinder拓展 */ - layerBinders: Set = new Set(); - - private needUpdate: boolean = false; - - static activeBinder: Set = new Set(); - - /** - * 绑定楼层为当前楼层,并跟随变化 - */ - bindThis() { - this.floor = void 0; - this.bindThisFloor = true; - this.layerBinders.forEach(v => v.bindThis()); - this.updateBind(); - } - - /** - * 绑定楼层为指定楼层 - * @param floorId 楼层id - */ - bindFloor(floorId: FloorIds) { - this.bindThisFloor = false; - this.floor = floorId; - this.layerBinders.forEach(v => v.bindFloor(floorId)); - this.updateBind(); - } - - /** - * 在下一帧进行绑定数据更新 - */ - updateBind() { - if (this.needUpdate || !this.group) return; - this.needUpdate = true; - this.group.requestBeforeFrame(() => { - this.needUpdate = false; - this.updateBindData(); - }); - } - - /** - * 立刻进行数据绑定更新 - */ - updateBindData() { - this.layerBinders.forEach(v => { - v.updateBindData(); - }); - - const floor = this.getFloor(); - this.emit('update', floor); - } - - getFloor() { - return this.bindThisFloor ? core.status.floorId : this.floor!; - } - - /** - * 设置图块 - */ - setBlock(layer: FloorLayer, block: AllNumbers, x: number, y: number) { - const ex = this.group - .getLayer(layer) - ?.getExtends('floor-binder') as LayerFloorBinder; - if (!ex) return; - ex.setBlock(block, x, y); - - const floor = this.bindThisFloor ? core.status.floorId : this.floor!; - this.emit('setBlock', x, y, floor, block); - } - - checkLayerExtends(layer: Layer) { - const ex = layer.getExtends('floor-binder'); - - if (!ex) { - const extend = new LayerFloorBinder(this); - layer.extends(extend); - this.layerBinders.add(extend); - } else { - if (ex instanceof LayerFloorBinder) { - ex.setParent(this); - this.layerBinders.add(ex); - } - } - } - - awake(group: LayerGroup) { - this.group = group; - - for (const layer of group.layers.values()) { - this.checkLayerExtends(layer); - } - LayerGroupFloorBinder.activeBinder.add(this); - } - - onLayerAdd(_group: LayerGroup, layer: Layer): void { - this.checkLayerExtends(layer); - } - - onDestroy(group: LayerGroup) { - LayerGroupFloorBinder.activeBinder.delete(this); - group.layers.forEach(v => { - v.removeExtends('floor-binder'); - }); - this.removeAllListeners(); - } -} - -/** - * 楼层绑定拓展,用于Layer的楼层渲染。 - * 注意,如果目标Layer是LayerGroup的子元素,那么会自动检测父元素是否包含LayerGroupFloorBinder拓展, - * 如果包含,那么会自动将此拓展附加至父元素的拓展。当父元素的拓展被移除时,此拓展也会一并被移除。 - */ -export class LayerFloorBinder implements ILayerRenderExtends { - id: string = 'floor-binder'; - - parent?: LayerGroupFloorBinder; - layer!: Layer; - bindThisFloor: boolean = true; - floor?: FloorIds; - - static listenedBinder: Set = new Set(); - - private needUpdate: boolean = false; - - constructor(parent?: LayerGroupFloorBinder) { - this.parent = parent; - } - - /** - * 绑定楼层为当前楼层,并跟随变化 - */ - bindThis() { - this.floor = void 0; - this.bindThisFloor = true; - this.updateBind(); - } - - /** - * 绑定楼层为指定楼层 - * @param floorId 楼层id - */ - bindFloor(floorId: FloorIds) { - this.bindThisFloor = false; - this.floor = floorId; - this.updateBind(); - } - - getFloor() { - return this.bindThisFloor ? core.status.floorId : this.floor!; - } - - /** - * 设置这个拓展附属至的父拓展(LayerGroupFloorBinder拓展) - * @param parent 父拓展 - */ - setParent(parent?: LayerGroupFloorBinder) { - this.parent = parent; - this.checkListen(); - } - - private checkListen() { - if (this.parent) LayerFloorBinder.listenedBinder.delete(this); - else LayerFloorBinder.listenedBinder.add(this); - } - - /** - * 在下一帧进行绑定数据更新 - */ - updateBind() { - if (this.needUpdate) return; - this.needUpdate = true; - this.layer.requestBeforeFrame(() => { - this.needUpdate = false; - this.updateBindData(); - }); - } - - /** - * 设置图块 - */ - setBlock(block: AllNumbers, x: number, y: number) { - this.layer.putRenderData([block], 1, x, y); - } - - /** - * 立刻更新绑定数据,而非下一帧 - */ - updateBindData() { - const floor = this.getFloor(); - if (!floor) return; - core.extractBlocks(floor); - const map = core.status.maps[floor]; - this.layer.setMapSize(map.width, map.height); - const image = core.status.maps[this.getFloor()].images; - if (this.layer.layer === 'event') { - const m = map.map; - this.layer.putRenderData(m.flat(), map.width, 0, 0); - } else { - const m = core.maps._getBgFgMapArray(this.layer.layer!, floor); - this.layer.putRenderData(m.flat(), map.width, 0, 0); - } - if (this.layer.layer === 'bg') { - // 别忘了背景图块 - this.layer.setBackground(texture.idNumberMap[map.defaultGround]); - } - const toDraw = image?.filter(v => v.canvas === this.layer.layer); - this.layer.setFloorImage(toDraw ?? []); - } - - awake(layer: Layer) { - this.layer = layer; - if (!this.parent) { - const group = layer.parent; - if (group instanceof LayerGroup) { - const ex = group.getExtends('floor-binder'); - if (ex instanceof LayerGroupFloorBinder) { - ex.checkLayerExtends(layer); - this.parent = ex; - } - } - } - this.checkListen(); - } - - onDestroy(_layer: Layer) { - LayerFloorBinder.listenedBinder.delete(this); - this.parent?.layerBinders.delete(this); - } -} - -interface DoorAnimateRenderable { - renderable: LayerMovingRenderable; - count: number; - perTime: number; -} - -export class LayerDoorAnimate implements ILayerRenderExtends { - id: string = 'door-animate'; - - layer!: Layer; - - private moving: Set = new Set(); - - private getRenderable(block: Block): DoorAnimateRenderable | null { - const { x, y, id } = block; - const renderable = texture.getRenderable(id); - if (!renderable) return null; - const image = renderable.autotile - ? renderable.image[0] - : renderable.image; - const time = block.event.doorInfo?.time ?? 160; - const frame = renderable.render.length; - const perTime = time / frame; - - const data: LayerMovingRenderable = { - x, - y, - zIndex: y, - image, - autotile: false, - animate: 0, - frame, - bigImage: false, - render: renderable.render, - alpha: 1 - }; - return { renderable: data, count: frame, perTime }; - } - - /** - * 开门 - * @param block 图块信息 - */ - async openDoor(block: Block) { - const renderable = this.getRenderable(block); - if (!renderable) return Promise.reject(); - const { renderable: data, count: frame, perTime } = renderable; - data.animate = 0; - this.moving.add(data); - this.layer.requestUpdateMoving(); - - let now = 0; - while (now < frame) { - await sleep(perTime); - data.animate = ++now; - this.layer.update(this.layer); - } - - this.moving.delete(data); - this.layer.requestUpdateMoving(); - return Promise.resolve(); - } - - /** - * 关门 - * @param block 图块信息 - */ - async closeDoor(block: Block) { - const renderable = this.getRenderable(block); - if (!renderable) return Promise.reject(); - const { renderable: data, count: frame, perTime } = renderable; - data.animate = frame - 1; - this.moving.add(data); - this.layer.requestUpdateMoving(); - - let now = 0; - while (now >= 0) { - await sleep(perTime); - data.animate = --now; - this.layer.update(this.layer); - } - this.moving.delete(data); - this.layer.requestUpdateMoving(); - return Promise.resolve(); - } - - awake(layer: Layer) { - this.layer = layer; - doorAdapter.add(this); - } - - onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void { - renderable.push(...this.moving); - } - - onDestroy(_layer: Layer): void { - doorAdapter.remove(this); - } -} - -const doorAdapter = new RenderAdapter('door-animate'); -doorAdapter.receive('openDoor', (item, block: Block) => { - return item.openDoor(block); -}); -doorAdapter.receive('closeDoor', (item, block: Block) => { - return item.closeDoor(block); -}); diff --git a/packages-user/client-modules/src/render/elements/misc.ts b/packages-user/client-modules/src/render/elements/misc.ts index 6614117..d57fe90 100644 --- a/packages-user/client-modules/src/render/elements/misc.ts +++ b/packages-user/client-modules/src/render/elements/misc.ts @@ -1,36 +1,49 @@ import { logger } from '@motajs/common'; import { - ERenderItemEvent, RenderItem, - RenderItemPosition, MotaOffscreenCanvas2D, Transform, SizedCanvasImageSource } from '@motajs/render'; import { isNil } from 'lodash-es'; import { RenderableData, AutotileRenderable, texture } from './cache'; -import { IAnimateFrame, renderEmits } from './frame'; +import { IExcitable } from '@motajs/animate'; +import { IMotaIcon, IMotaWinskin } from './types'; -export interface EIconEvent extends ERenderItemEvent {} - -export class Icon extends RenderItem implements IAnimateFrame { +export class Icon extends RenderItem implements IMotaIcon, IExcitable { /** 图标id */ icon: AllNumbers = 0; - /** 帧数 */ + /** 渲染动画的第几帧 */ frame: number = 0; /** 是否启用动画 */ animate: boolean = false; + /** 当前动画速度 */ + frameSpeed: number = 300; + /** 当前帧率 */ + nowFrame: number = 0; + /** 图标的渲染信息 */ private renderable?: RenderableData | AutotileRenderable; private pendingIcon?: AllNumbers; - constructor(type: RenderItemPosition, cache?: boolean, fall?: boolean) { - super(type, cache, fall); + /** 委托激励对象 id,用于图标的动画展示 */ + private delegation: number = -1; + + constructor(cache: boolean = false) { + super(cache); this.setAntiAliasing(false); this.setHD(false); } + excited(payload: number): void { + if (!this.renderable) return; + const frame = Math.floor(payload / 300); + if (frame === this.nowFrame) return; + this.nowFrame = frame; + this.update(); + } + protected render( canvas: MotaOffscreenCanvas2D, _transform: Transform @@ -42,7 +55,7 @@ export class Icon extends RenderItem implements IAnimateFrame { const cw = this.width; const ch = this.height; const frame = this.animate - ? RenderItem.animatedFrame % renderable.frame + ? this.nowFrame % renderable.frame : this.frame; if (!this.animate) { @@ -87,6 +100,27 @@ export class Icon extends RenderItem implements IAnimateFrame { } } + setFrameSpeed(speed: number): void { + this.frameSpeed = speed; + this.update(); + } + + setFrame(frame: number): void { + if (frame < 0) { + this.setAnimateStatus(true); + return; + } + this.frame = frame; + this.update(); + } + + setAnimateStatus(animate: boolean): void { + this.animate = animate; + if (!animate) this.removeExcitable(this.delegation); + else this.delegation = this.delegateExcitable(this); + this.update(); + } + private setIconRenderable(num: AllNumbers) { const renderable = texture.getRenderable(num); @@ -101,15 +135,7 @@ export class Icon extends RenderItem implements IAnimateFrame { this.update(); } - /** - * 更新动画帧 - */ - updateFrameAnimate(): void { - if (this.animate) this.update(this); - } - destroy(): void { - renderEmits.removeFramer(this); super.destroy(); } @@ -124,142 +150,50 @@ export class Icon extends RenderItem implements IAnimateFrame { return true; case 'animate': if (!this.assertType(nextValue, 'boolean', key)) return false; - this.animate = nextValue; - if (nextValue) renderEmits.addFramer(this); - else renderEmits.removeFramer(this); - this.update(); + this.setAnimateStatus(nextValue); return true; case 'frame': if (!this.assertType(nextValue, 'number', key)) return false; - this.frame = nextValue; - this.update(); + this.setFrame(nextValue); + return true; + case 'speed': + if (!this.assertType(nextValue, 'number', key)) return false; + this.setFrameSpeed(nextValue); return true; } return false; } } -interface WinskinPatterns { - top: CanvasPattern; - left: CanvasPattern; - bottom: CanvasPattern; - right: CanvasPattern; -} - -export interface EWinskinEvent extends ERenderItemEvent {} - -export class Winskin extends RenderItem { - image: SizedCanvasImageSource; +export class Winskin extends RenderItem implements IMotaWinskin { + image: SizedCanvasImageSource | null = null; /** 边框宽度,32表示原始宽度 */ borderSize: number = 32; /** 图片名称 */ - imageName?: string; + imageName: string = ''; private pendingImage?: ImageIds; - private patternCache?: WinskinPatterns; - private patternTransform: DOMMatrix; - // todo: 跨上下文可能是未定义行为,需要上下文无关化 - private static patternMap: Map = new Map(); - - constructor( - image: SizedCanvasImageSource, - type: RenderItemPosition = 'static' - ) { - super(type, false, false); - this.image = image; + constructor(enableCache: boolean = false) { + super(enableCache); this.setAntiAliasing(false); - - if (window.DOMMatrix) { - this.patternTransform = new DOMMatrix(); - } else if (window.WebKitCSSMatrix) { - this.patternTransform = new WebKitCSSMatrix(); - } else { - this.patternTransform = new SVGMatrix(); - } } - private generatePattern() { - const pattern = this.requireCanvas(true, false); - pattern.setScale(1); + protected render(canvas: MotaOffscreenCanvas2D): void { const img = this.image; - pattern.size(32, 16); - pattern.setHD(false); - pattern.setAntiAliasing(false); - const ctx = pattern.ctx; - ctx.drawImage(img, 144, 0, 32, 16, 0, 0, 32, 16); - const topPattern = ctx.createPattern(pattern.canvas, 'repeat'); - ctx.clearRect(0, 0, 32, 16); - ctx.drawImage(img, 144, 48, 32, 16, 0, 0, 32, 16); - const bottomPattern = ctx.createPattern(pattern.canvas, 'repeat'); - ctx.clearRect(0, 0, 32, 16); - pattern.size(16, 32); - ctx.drawImage(img, 128, 16, 16, 32, 0, 0, 16, 32); - const leftPattern = ctx.createPattern(pattern.canvas, 'repeat'); - ctx.clearRect(0, 0, 16, 32); - ctx.drawImage(img, 176, 16, 16, 32, 0, 0, 16, 32); - const rightPattern = ctx.createPattern(pattern.canvas, 'repeat'); - if (!topPattern || !bottomPattern || !leftPattern || !rightPattern) { - return null; - } - const winskinPattern: WinskinPatterns = { - top: topPattern, - bottom: bottomPattern, - left: leftPattern, - right: rightPattern - }; - if (this.imageName) { - Winskin.patternMap.set(this.imageName, winskinPattern); - } - this.patternCache = winskinPattern; - this.deleteCanvas(pattern); - return winskinPattern; - } - - private getPattern() { - if (!this.imageName) { - if (this.patternCache) return this.patternCache; - return this.generatePattern(); - } else { - const pattern = Winskin.patternMap.get(this.imageName); - if (pattern) return pattern; - return this.generatePattern(); - } - } - - protected render( - canvas: MotaOffscreenCanvas2D, - _transform: Transform - ): void { + if (!img) return; const ctx = canvas.ctx; - const img = this.image; const w = this.width; const h = this.height; const pad = this.borderSize / 2; // 背景 ctx.drawImage(img, 0, 0, 128, 128, 2, 2, w - 4, h - 4); - const pattern = this.getPattern(); - if (!pattern) return; - const { top, left, right, bottom } = pattern; - top.setTransform(this.patternTransform); - left.setTransform(this.patternTransform); - right.setTransform(this.patternTransform); - bottom.setTransform(this.patternTransform); // 上下左右边框 ctx.save(); - ctx.fillStyle = top; - ctx.translate(pad, 0); - ctx.fillRect(0, 0, w - pad * 2, pad); - ctx.fillStyle = bottom; - ctx.translate(0, h - pad); - ctx.fillRect(0, 0, w - pad * 2, pad); - ctx.fillStyle = left; - ctx.translate(-pad, pad * 2 - h); - ctx.fillRect(0, 0, pad, h - pad * 2); - ctx.fillStyle = right; - ctx.translate(w - pad, 0); - ctx.fillRect(0, 0, pad, h - pad * 2); - ctx.restore(); + ctx.drawImage(img, 144, 0, 32, 16, pad, 0, w - pad * 2, pad); + ctx.drawImage(img, 144, 48, 32, 16, pad, h - pad, w - pad * 2, pad); + ctx.drawImage(img, 128, 16, 16, 32, 0, pad, pad, h - pad * 2); + ctx.drawImage(img, 176, 16, 16, 32, w - pad, pad, pad, h - pad * 2); // 四个角的边框 ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad); ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad); @@ -273,7 +207,7 @@ export class Winskin extends RenderItem { */ setImage(image: SizedCanvasImageSource) { this.image = image; - this.patternCache = void 0; + this.imageName = ''; this.update(); } @@ -307,8 +241,6 @@ export class Winskin extends RenderItem { */ setBorderSize(size: number) { this.borderSize = size; - this.patternTransform.a = size / 32; - this.patternTransform.d = size / 32; this.update(); } @@ -319,6 +251,9 @@ export class Winskin extends RenderItem { ): boolean { switch (key) { case 'image': + this.setImage(nextValue); + return true; + case 'imageName': if (!this.assertType(nextValue, 'string', key)) return false; this.setImageByName(nextValue); return true; diff --git a/packages-user/client-modules/src/render/elements/props.ts b/packages-user/client-modules/src/render/elements/props.ts index 095fbad..ce4da18 100644 --- a/packages-user/client-modules/src/render/elements/props.ts +++ b/packages-user/client-modules/src/render/elements/props.ts @@ -1,29 +1,7 @@ import { BaseProps, TagDefine } from '@motajs/render-vue'; -import { ERenderItemEvent, Transform, CanvasStyle } from '@motajs/render'; -import { - ILayerGroupRenderExtends, - FloorLayer, - ILayerRenderExtends, - ELayerEvent, - ELayerGroupEvent -} from './layer'; -import { EAnimateEvent } from './animate'; -import { EIconEvent, EWinskinEvent } from './misc'; -import { IEnemyCollection } from '@motajs/types'; +import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render'; import { ILayerState } from '@user/data-state'; -import { IMapExtensionManager, IMapRenderer, IOnMapTextRenderer } from '../map'; - -export interface AnimateProps extends BaseProps {} - -export interface DamageProps extends BaseProps { - mapWidth?: number; - mapHeight?: number; - cellSize?: number; - enemy?: IEnemyCollection; - font?: string; - strokeStyle?: CanvasStyle; - strokeWidth?: number; -} +import { IMapExtensionManager, IMapRenderer } from '../map'; export interface IconProps extends BaseProps { /** 图标 id 或数字 */ @@ -32,50 +10,30 @@ export interface IconProps extends BaseProps { frame?: number; /** 是否开启动画,开启后 frame 参数无效 */ animate?: boolean; + /** 动画速度 */ + speed?: number; } export interface WinskinProps extends BaseProps { - /** winskin 的图片 id */ - image: ImageIds; + /** 直接设置 winskin 图片 */ + image?: SizedCanvasImageSource; + /** 根据图片名称设置 winskin 图片 */ + imageName?: string; /** 边框大小 */ borderSize?: number; } -export interface LayerGroupProps extends BaseProps { - cellSize?: number; - blockSize?: number; - floorId?: FloorIds; - bindThisFloor?: boolean; - camera?: Transform; - ex?: readonly ILayerGroupRenderExtends[]; - layers?: readonly FloorLayer[]; -} - -export interface LayerProps extends BaseProps { - layer?: FloorLayer; - mapWidth?: number; - mapHeight?: number; - cellSize?: number; - background?: AllNumbers; - floorImage?: FloorAnimate[]; - ex?: readonly ILayerRenderExtends[]; -} - export interface MapRenderProps extends BaseProps { layerState: ILayerState; renderer: IMapRenderer; extension: IMapExtensionManager; - textExtension?: IOnMapTextRenderer | null; } declare module 'vue/jsx-runtime' { namespace JSX { export interface IntrinsicElements { - layer: TagDefine; - 'layer-group': TagDefine; - animation: TagDefine; - icon: TagDefine; - winskin: TagDefine; + icon: TagDefine; + winskin: TagDefine; 'map-render': TagDefine; } } diff --git a/packages-user/client-modules/src/render/elements/types.ts b/packages-user/client-modules/src/render/elements/types.ts new file mode 100644 index 0000000..3b2fa25 --- /dev/null +++ b/packages-user/client-modules/src/render/elements/types.ts @@ -0,0 +1,65 @@ +import { IRenderItem, SizedCanvasImageSource } from '@motajs/render'; + +export interface IMotaIcon extends IRenderItem { + /** 图标id */ + readonly icon: AllNumbers; + /** 渲染动画的第几帧 */ + readonly frame: number; + /** 是否启用动画 */ + readonly animate: boolean; + /** 当前动画帧数,如果没有启用动画则为 -1 */ + readonly nowFrame: number; + /** 当前图标的动画速度,每多长时间切换至下一帧,单位毫秒 */ + readonly frameSpeed: number; + + /** + * 设置图标 + * @param id 图标id + */ + setIcon(id: AllIdsWithNone | AllNumbers): void; + + /** + * 设置当前图标的帧动画速度,单位毫秒 + * @param speed 帧动画速度 + */ + setFrameSpeed(speed: number): void; + + /** + * 设置当前图标是否启用帧动画 + * @param animate 是否启用帧动画 + */ + setAnimateStatus(animate: boolean): void; + + /** + * 设置图标显示第几帧,如果传入负数视为启用帧动画 + * @param frame 显示图标的第几帧 + */ + setFrame(frame: number): void; +} + +export interface IMotaWinskin extends IRenderItem { + /** winskin 图片源 */ + readonly image: SizedCanvasImageSource | null; + /** 边框尺寸 */ + readonly borderSize: number; + /** winskin 图片名称,如果不是使用名称设置的图片的话,此值为空字符串 */ + readonly imageName: string; + + /** + * 设置winskin图片 + * @param image winskin图片 + */ + setImage(image: SizedCanvasImageSource): void; + + /** + * 通过图片名称设置winskin + * @param name 图片名称 + */ + setImageByName(name: ImageIds): void; + + /** + * 设置边框大小 + * @param size 边框大小 + */ + setBorderSize(size: number): void; +} diff --git a/packages-user/client-modules/src/render/elements/utils.ts b/packages-user/client-modules/src/render/elements/utils.ts deleted file mode 100644 index f897cdd..0000000 --- a/packages-user/client-modules/src/render/elements/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RenderAdapter } from '@motajs/render'; -import { FloorViewport } from './viewport'; - -export function disableViewport() { - const adapter = RenderAdapter.get('viewport'); - if (!adapter) return; - adapter.sync('disable'); -} - -export function enableViewport() { - const adapter = RenderAdapter.get('viewport'); - if (!adapter) return; - adapter.sync('enable'); -} diff --git a/packages-user/client-modules/src/render/elements/viewport.ts b/packages-user/client-modules/src/render/elements/viewport.ts deleted file mode 100644 index 74adfd1..0000000 --- a/packages-user/client-modules/src/render/elements/viewport.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { RenderAdapter } from '@motajs/render'; -import { HeroRenderer } from './hero'; -import { ILayerGroupRenderExtends, LayerGroup } from './layer'; -import { LayerGroupFloorBinder } from './layer'; -import { hyper, TimingFn } from 'mutate-animate'; -import { - MAP_BLOCK_HEIGHT, - MAP_BLOCK_WIDTH, - MAP_HEIGHT, - MAP_WIDTH -} from '../shared'; - -export class FloorViewport implements ILayerGroupRenderExtends { - id: string = 'viewport'; - - group!: LayerGroup; - hero?: HeroRenderer; - binder?: LayerGroupFloorBinder; - - /** 是否启用视角控制拓展 */ - enabled: boolean = true; - /** 是否自动限定视角范围至地图范围 */ - boundX: boolean = true; - boundY: boolean = true; - - /** 渐变的速率曲线 */ - transitionFn: TimingFn = hyper('sin', 'out'); - /** 瞬移的速率曲线 */ - mutateFn: TimingFn = hyper('sin', 'out'); - - /** 突变时的渐变时长 */ - transitionTime: number = 600; - - /** 当前视角位置 */ - private nx: number = 0; - private ny: number = 0; - /** 移动时的偏移位置 */ - private ox: number = 0; - private oy: number = 0; - /** 移动时的偏移最大值 */ - private maxOffset: number = 1; - - /** 委托ticker */ - private delegation: number = -1; - /** 渐变委托ticker */ - private transition: number = -1; - /** 移动委托ticker */ - private moving: number = -1; - /** 是否在渐变过程中 */ - private inTransition: boolean = false; - /** 是否在移动过程中 */ - private inMoving: boolean = false; - - /** 移动的监听函数 */ - private movingFramer?: () => void; - - /** - * 禁用自动视角控制 - */ - disable() { - this.enabled = false; - } - - /** - * 启用自动视角控制 - */ - enable() { - this.enabled = true; - const { x, y } = core.status.hero.loc; - const { x: nx, y: ny } = this.group.camera; - const halfWidth = MAP_WIDTH / 2; - const halfHeight = MAP_HEIGHT / 2; - const cell = this.group.cellSize; - const half = cell / 2; - this.applyPosition( - -(nx - halfWidth + half) / this.group.cellSize, - -(ny - halfHeight + half) / this.group.cellSize - ); - this.mutateTo(x, y); - } - - /** - * 设置是否自动限定视角范围至地图范围 - * @param boundX 是否自动限定水平视角范围 - * @param boundY 是否自动限定竖直视角范围 - */ - setAutoBound(boundX: boolean = this.boundX, boundY: boolean = this.boundY) { - this.boundX = boundX; - this.boundY = boundY; - this.group.requestBeforeFrame(() => { - this.setPosition(this.nx, this.ny); - }); - } - - /** - * 传入视角的目标位置,将其限定在地图范围内后返回 - * @param x 图格横坐标 - * @param y 图格纵坐标 - */ - getBoundedPosition(x: number, y: number) { - if (!this.checkDependency()) return { x, y }; - if (!this.boundX && !this.boundY) return { x, y }; - const width = MAP_BLOCK_WIDTH; - const height = MAP_BLOCK_HEIGHT; - const minX = (width - 1) / 2; - const minY = (height - 1) / 2; - const floor = core.status.maps[this.binder!.getFloor()]; - const maxX = floor.width - minX - 1; - const maxY = floor.height - minY - 1; - - return { - x: this.boundX ? core.clamp(x, minX, maxX) : x, - y: this.boundY ? core.clamp(y, minY, maxY) : y - }; - } - - /** - * 设置视角位置 - * @param x 目标图格横坐标 - * @param y 目标图格纵坐标 - */ - setPosition(x: number, y: number) { - if (!this.enabled) return; - const { x: nx, y: ny } = this.getBoundedPosition(x, y); - this.group.removeTicker(this.transition, false); - this.applyPosition(nx, ny); - } - - /** - * 开始移动 - */ - startMove() { - if (this.inMoving) return; - this.inMoving = true; - this.createMoveTransition(); - } - - /** - * 结束移动 - */ - endMove() { - this.inMoving = false; - } - - /** - * 当勇士通过移动改变至指定位置时移动视角 - * @param x 目标图格横坐标 - * @param y 目标图格纵坐标 - */ - moveTo(x: number, y: number, time: number = 200) { - if (!this.enabled) return; - const { x: nx, y: ny } = this.getBoundedPosition(x, y); - if (this.inTransition) { - const distance = Math.hypot(this.nx - nx, this.ny - ny); - const t = core.clamp(distance * time, time, time * 3); - this.createTransition(nx, ny, t, this.transitionFn); - } - } - - private createMoveTransition() { - if (!this.checkDependency()) return; - let xTarget: number = 0; - let yTarget: number = 0; - let xStart: number = this.ox; - let yStart: number = this.oy; - let xStartTime: number = Date.now(); - let yStartTime: number = Date.now(); - let ending: boolean = false; - // 这个数等于 sinh(2),用这个数的话,可以正好在刚开始移动的时候达到1的斜率,效果会比较好 - const transitionTime = this.hero!.speed * 3.626860407847019; - - const setTargetX = (x: number, time: number) => { - if (x === xTarget) return; - xTarget = x; - xStartTime = time; - xStart = this.ox; - }; - const setTargetY = (y: number, time: number) => { - if (y === yTarget) return; - yTarget = y; - yStart = this.oy; - yStartTime = time; - }; - - if (this.movingFramer) { - this.hero!.off('moveTick', this.movingFramer); - } - this.movingFramer = () => { - if (this.inTransition) return; - const now = Date.now(); - if (!this.inMoving && !ending) { - setTargetX(0, now); - setTargetY(0, now); - ending = true; - } - if (!ending) { - const dir = this.hero!.stepDir; - const { x, y } = core.utils.scan2[dir]; - setTargetX(-x * this.maxOffset, now); - setTargetY(-y * this.maxOffset, now); - } - - if (!this.hero!.renderable) return; - - const { x, y } = this.hero!.renderable; - const { x: nx, y: ny } = this.getBoundedPosition(x, y); - this.applyPosition(nx, ny); - - if (ending) { - if (this.ox === xTarget && this.oy === yTarget) { - this.hero!.off('moveTick', this.movingFramer); - return; - } - } - // todo: 效果太差了,需要优化 - return; - if (this.ox !== xTarget) { - const time = transitionTime * Math.abs(xStart - xTarget); - const progress = (now - xStartTime) / time; - if (progress > 1) { - this.ox = xTarget; - } else { - const p = this.transitionFn(progress); - this.ox = (xTarget - xStart) * p + xStart; - } - } - if (this.oy !== yTarget) { - const time = transitionTime * Math.abs(yStart - yTarget); - const progress = (now - yStartTime) / time; - if (progress > 1) { - this.oy = yTarget; - } else { - const p = this.transitionFn(progress); - this.oy = (yTarget - yStart) * p + yStart; - } - } - }; - this.hero!.on('moveTick', this.movingFramer); - } - - /** - * 当勇士位置突变至指定位置时移动视角 - * @param x 目标图格横坐标 - * @param y 目标图格纵坐标 - */ - mutateTo(x: number, y: number, time: number = this.transitionTime) { - if (!this.enabled) return; - const { x: nx, y: ny } = this.getBoundedPosition(x, y); - this.createTransition(nx, ny, time, this.mutateFn); - } - - private createTransition(x: number, y: number, time: number, fn: TimingFn) { - const start = Date.now(); - const end = start + time; - const sx = this.nx; - const sy = this.ny; - const dx = x - sx; - const dy = y - sy; - - this.inTransition = true; - this.group.removeTicker(this.transition, false); - this.transition = this.group.delegateTicker( - () => { - const now = Date.now(); - if (now >= end) { - this.group.removeTicker(this.transition, true); - return; - } - const progress = fn((now - start) / time); - const tx = dx * progress; - const ty = dy * progress; - this.applyPosition(tx + sx, ty + sy); - }, - time, - () => { - this.applyPosition(x, y); - this.inTransition = false; - } - ); - } - - private applyPosition(x: number, y: number) { - if (!this.enabled) return; - if (x === this.nx && y === this.ny) return; - const halfWidth = MAP_WIDTH / 2; - const halfHeight = MAP_HEIGHT / 2; - const cell = this.group.cellSize; - const half = cell / 2; - this.nx = x; - this.ny = y; - const { x: bx, y: by } = this.getBoundedPosition(x, y); - const rx = bx * cell - halfWidth + half; - const ry = by * cell - halfHeight + half; - core.bigmap.offsetX = rx; - core.bigmap.offsetY = ry; - this.group.camera.setTranslate(-rx, -ry); - this.group.update(this.group); - } - - private checkDependency() { - if (this.hero && this.binder) return true; - const group = this.group; - const ex1 = group.getLayer('event')?.getExtends('floor-hero'); - const ex2 = group.getExtends('floor-binder'); - if ( - ex1 instanceof HeroRenderer && - ex2 instanceof LayerGroupFloorBinder - ) { - this.hero = ex1; - this.binder = ex2; - return true; - } - return false; - } - - awake(group: LayerGroup): void { - this.group = group; - adapter.add(this); - } - - onDestroy(group: LayerGroup): void { - group.removeTicker(this.delegation); - group.removeTicker(this.transition); - group.removeTicker(this.moving); - adapter.remove(this); - } -} - -const adapter = new RenderAdapter('viewport'); -adapter.receive('mutateTo', (item, x, y, time) => { - item.mutateTo(x, y, time); - return Promise.resolve(); -}); -adapter.receive('moveTo', (item, x, y, time) => { - item.moveTo(x, y, time); - return Promise.resolve(); -}); -adapter.receive('setPosition', (item, x, y) => { - item.setPosition(x, y); - return Promise.resolve(); -}); -adapter.receiveSync('disable', item => { - item.disable(); -}); -adapter.receiveSync('enable', item => { - item.enable(); -}); -adapter.receiveSync('startMove', item => { - item.startMove(); -}); -adapter.receiveSync('endMove', item => { - item.endMove(); -}); - -export function createViewport() { - const { hook } = Mota.require('@user/data-base'); - hook.on('changingFloor', (_, loc) => { - adapter.all('setPosition', loc.x, loc.y); - }); -} diff --git a/packages-user/client-modules/src/render/index.tsx b/packages-user/client-modules/src/render/index.tsx index c4c7ae8..2fc27a4 100644 --- a/packages-user/client-modules/src/render/index.tsx +++ b/packages-user/client-modules/src/render/index.tsx @@ -10,7 +10,7 @@ import { sceneController } from './scene'; import { GameTitleUI } from './ui/title'; import { createWeather } from './weather'; import { createMainExtension } from './commonIns'; -import { createApp } from '@motajs/render-vue'; +import { createApp } from './renderer'; export function createGameRenderer() { const App = defineComponent(_props => { diff --git a/packages-user/client-modules/src/render/legacy/fallback.ts b/packages-user/client-modules/src/render/legacy/fallback.ts deleted file mode 100644 index 7e7dbb1..0000000 --- a/packages-user/client-modules/src/render/legacy/fallback.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { - MotaOffscreenCanvas2D, - RenderItem, - RenderItemPosition, - Transform -} from '@motajs/render'; -import { wrapInstancedComponent } from '@motajs/render-vue'; - -// 渲染端的向后兼容用,会充当两个版本间过渡的作用 -class Change extends RenderItem { - private tips: string[] = []; - /** 当前小贴士 */ - private usingTip: string = ''; - /** 透明度 */ - private backAlpha: number = 0; - private title: string = ''; - - constructor(type: RenderItemPosition) { - super(type, false); - } - - /** - * 设置楼传过程中的小贴士 - */ - setTips(tip: string[]) { - this.tips = tip; - } - - /** - * 设置标题 - */ - setTitle(title: string) { - this.title = title; - } - - /** - * 显示楼层切换的从透明变黑的动画 - * @param time 动画时长 - */ - showChange(time: number) { - const length = this.tips.length; - const tip = this.tips[Math.floor(Math.random() * length)] ?? ''; - this.usingTip = tip; - - return new Promise(res => { - const start = Date.now(); - const id = this.delegateTicker( - () => { - const dt = Date.now() - start; - const progress = dt / time; - if (progress > 1) { - this.backAlpha = 1; - this.removeTicker(id); - } else { - this.backAlpha = progress; - } - this.update(); - }, - 10000, - res - ); - }); - } - - /** - * 显示楼层切换从黑到透明的动画 - * @param time 动画时长 - */ - async hideChange(time: number) { - return new Promise(res => { - const start = Date.now(); - const id = this.delegateTicker( - () => { - const dt = Date.now() - start; - const progress = dt / time; - if (progress > 1) { - this.removeTicker(id); - this.backAlpha = 0; - } else { - this.backAlpha = 1 - progress; - } - this.update(); - }, - 10000, - res - ); - }); - } - - protected render( - canvas: MotaOffscreenCanvas2D, - _transform: Transform - ): void { - if (this.backAlpha === 0) return; - const ctx = canvas.ctx; - ctx.globalAlpha = this.backAlpha; - ctx.fillStyle = '#000'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.textAlign = 'center'; - ctx.fillStyle = '#fff'; - ctx.font = '32px "normal"'; - ctx.fillText(this.title, canvas.width / 2, canvas.height * 0.4); - ctx.font = '16px "normal"'; - if (this.usingTip.length > 0) { - ctx.fillText( - '小贴士:' + this.usingTip, - canvas.width / 2, - canvas.height * 0.75 - ); - } - } -} - -export const FloorChange = wrapInstancedComponent(() => new Change('static')); diff --git a/packages-user/client-modules/src/render/map/element.ts b/packages-user/client-modules/src/render/map/element.ts index 10624b0..7274f90 100644 --- a/packages-user/client-modules/src/render/map/element.ts +++ b/packages-user/client-modules/src/render/map/element.ts @@ -5,7 +5,7 @@ import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared'; import { IMapExtensionManager } from './extension'; -export class MapRender extends RenderItem { +export class MapRenderItem extends RenderItem { /** * @param layerState 地图状态对象 * @param renderer 地图渲染器对象 @@ -15,13 +15,14 @@ export class MapRender extends RenderItem { readonly renderer: IMapRenderer, readonly exManager: IMapExtensionManager ) { - super('static', false, false); + super(false); renderer.setLayerState(layerState); renderer.setCellSize(CELL_WIDTH, CELL_HEIGHT); renderer.setRenderSize(MAP_WIDTH, MAP_HEIGHT); - this.delegateTicker(time => { + // 元素被销毁时会自动删除所有的激励对象,所以不需要担心会内存泄漏 + this.delegateExcitable(time => { this.renderer.tick(time); if (this.renderer.needUpdate()) { this.update(); diff --git a/packages-user/client-modules/src/render/map/types.ts b/packages-user/client-modules/src/render/map/types.ts index cc5b3c5..8b93aa0 100644 --- a/packages-user/client-modules/src/render/map/types.ts +++ b/packages-user/client-modules/src/render/map/types.ts @@ -997,8 +997,7 @@ export interface IMapVertexStatus { * 脏标记表示顶点数组的长度是否发生变化 */ export interface IMapVertexGenerator - extends IDirtyTracker, - IMapVertexStatus { + extends IDirtyTracker, IMapVertexStatus { /** 地图渲染器 */ readonly renderer: IMapRenderer; /** 地图分块 */ diff --git a/packages-user/client-modules/src/render/renderer.ts b/packages-user/client-modules/src/render/renderer.ts index 82aa689..65c3be0 100644 --- a/packages-user/client-modules/src/render/renderer.ts +++ b/packages-user/client-modules/src/render/renderer.ts @@ -1,8 +1,46 @@ import { MotaRenderer } from '@motajs/render'; -import { MAIN_WIDTH, MAIN_HEIGHT } from './shared'; +import { + MAIN_WIDTH, + MAIN_HEIGHT, + DEBUG_VARIATOR, + VARIATOR_DEBUG_SPEED, + DEBUG_DIVIDER, + DIVIDER_DEBUG_DIVIDER +} from './shared'; +import { createRendererFor, RendererUsing } from '@motajs/render-vue'; +import { + ExcitationDivider, + ExcitationVariator, + RafExcitation +} from '@motajs/animate'; + +/** 渲染激励源 */ +export const rafExcitation = new RafExcitation(); +/** 渲染分频器 */ +export const excitationDivider = new ExcitationDivider(); + +if (DEBUG_VARIATOR) { + const variator = new ExcitationVariator(); + variator.bindExcitation(rafExcitation); + variator.setSpeed(VARIATOR_DEBUG_SPEED); + excitationDivider.bindExcitation(variator); +} else { + excitationDivider.bindExcitation(rafExcitation); +} + +if (DEBUG_DIVIDER) { + excitationDivider.setDivider(DIVIDER_DEBUG_DIVIDER); +} export const mainRenderer = new MotaRenderer({ canvas: '#render-main', width: MAIN_WIDTH, - height: MAIN_HEIGHT + height: MAIN_HEIGHT, + // 使用分频器,用户可以在设置中调整,如果设备性能较差调高分频有助于提高性能表现 + excitaion: excitationDivider }); + +export const using = new RendererUsing(mainRenderer); + +export const { createApp, render, tagManager } = + createRendererFor(mainRenderer); diff --git a/packages-user/client-modules/src/render/shared.ts b/packages-user/client-modules/src/render/shared.ts index fc4f51b..9c0da05 100644 --- a/packages-user/client-modules/src/render/shared.ts +++ b/packages-user/client-modules/src/render/shared.ts @@ -2,6 +2,25 @@ import { ElementLocator, Font } from '@motajs/render'; // 本文件为 UI 配置文件,你可以修改下面的每个常量来控制 UI 的显示参数,每个常量都有注释说明 +//#region 调试用参数 + +/** + * 渲染器的激励源是否使用变速器,使用后可以通过 {@link VARIATOR_DEBUG_SPEED} 调整激励源速度, + * 从而调整动画等内容的执行速度,方便调试 + */ +export const DEBUG_VARIATOR = false; +/** 当使用变速器作为激励源调试时,变速器的速度 */ +export const VARIATOR_DEBUG_SPEED = 0.2; +/** + * 是否使用分频器调试,使用后可以通过 {@link DIVIDER_DEBUG_DIVIDER} 调整分配比例, + * 降低画面刷新频率及每帧执行函数的执行频率,方便调试 + */ +export const DEBUG_DIVIDER = false; +/** 当使用分频器调试时,分配比例 */ +export const DIVIDER_DEBUG_DIVIDER = 60; + +//#endregion + //#region 地图 /** 每个格子的默认宽度,现阶段用处不大 */ diff --git a/packages-user/client-modules/src/render/ui/main.tsx b/packages-user/client-modules/src/render/ui/main.tsx index 4813f7d..2cacd41 100644 --- a/packages-user/client-modules/src/render/ui/main.tsx +++ b/packages-user/client-modules/src/render/ui/main.tsx @@ -2,7 +2,7 @@ import { Font, IActionEvent, MotaOffscreenCanvas2D, - Sprite + CustomRenderItem } from '@motajs/render'; // import { WeatherController } from '../weather'; import { defineComponent, onUnmounted, reactive, ref } from 'vue'; @@ -27,11 +27,10 @@ import { import { ReplayingStatus } from './toolbar'; import { getHeroStatusOn, state } from '@user/data-state'; import { hook } from '@user/data-base'; -import { FloorChange } from '../legacy/fallback'; import { mainUIController } from './controller'; import { isNil } from 'lodash-es'; import { mainMapExtension, mainMapRenderer } from '../commonIns'; -import { onTick } from '@motajs/render-vue'; +import { using } from '../renderer'; const MainScene = defineComponent(() => { //#region 基本定义 @@ -147,7 +146,7 @@ const MainScene = defineComponent(() => { //#region sprite 渲染 let lastLength = 0; - onTick(() => { + using.onExcitedFunc(() => { const len = core.status.stepPostfix?.length ?? 0; if (len !== lastLength) { mapMiscSprite.value?.update(); @@ -155,7 +154,7 @@ const MainScene = defineComponent(() => { } }); - const mapMiscSprite = ref(); + const mapMiscSprite = ref(); const renderMapMisc = (canvas: MotaOffscreenCanvas2D) => { const step = core.status.stepPostfix; @@ -245,7 +244,7 @@ const MainScene = defineComponent(() => { loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]} /> - + {/* */} { pad={[12, 6]} corner={16} /> - , - UIComponentProps { + extends Partial, UIComponentProps { loc: ElementLocator; } diff --git a/packages-user/client-modules/src/render/ui/title.tsx b/packages-user/client-modules/src/render/ui/title.tsx index 87ee433..d9f55bf 100644 --- a/packages-user/client-modules/src/render/ui/title.tsx +++ b/packages-user/client-modules/src/render/ui/title.tsx @@ -27,12 +27,13 @@ import { transitionedColor, useKey } from '../use'; -import { hyper, linear, sleep } from 'mutate-animate'; import { ExitFullscreen, Fullscreen, SoundVolume } from '../components'; import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui'; import { saveLoad } from './save'; import { MainSceneUI } from './main'; import { adjustCover } from '../utils'; +import { cosh, CurveMode, linear } from '@motajs/animate'; +import { sleep } from '@motajs/common'; const enum TitleButton { StartGame, @@ -106,8 +107,12 @@ export const GameTitle = defineComponent(props => { color: v.color, name: v.name, hard: '', - colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!, - scale: transitioned(1, 400, hyper('sin', 'out'))! + colorTrans: transitionedColor( + '#fff', + 400, + cosh(2, CurveMode.EaseOut) + )!, + scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))! }; }); @@ -118,8 +123,12 @@ export const GameTitle = defineComponent(props => { color: core.arrayToRGBA(v.color!), name: v.title, hard: v.name, - colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!, - scale: transitioned(1, 400, hyper('sin', 'out'))! + colorTrans: transitionedColor( + '#fff', + 400, + cosh(2, CurveMode.EaseOut) + )!, + scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))! }; }); // 返回按钮 @@ -128,11 +137,15 @@ export const GameTitle = defineComponent(props => { color: '#aaa', name: '返回', hard: '', - colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))! + colorTrans: transitionedColor('#fff', 400, cosh(2, CurveMode.EaseOut))! }); /** 声音设置按钮的颜色 */ - const soundColor = transitionedColor('#ddd', 400, hyper('sin', 'out'))!; + const soundColor = transitionedColor( + '#ddd', + 400, + cosh(2, CurveMode.EaseOut) + )!; /** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */ const buttonsAlpha = transitioned(1, 300, linear())!; @@ -152,7 +165,11 @@ export const GameTitle = defineComponent(props => { /** 选择难度界面按钮的高度 */ const hardHeight = (hard.length - 1) * 40 + 60; /** 按钮的背景框高度 */ - const rectHeight = transitioned(buttonHeight, 600, hyper('sin', 'in-out'))!; + const rectHeight = transitioned( + buttonHeight, + 600, + cosh(2, CurveMode.EaseOut) + )!; //#region 按钮功能 diff --git a/packages-user/client-modules/src/render/ui/viewmap.tsx b/packages-user/client-modules/src/render/ui/viewmap.tsx index 1fb600e..506cc5d 100644 --- a/packages-user/client-modules/src/render/ui/viewmap.tsx +++ b/packages-user/client-modules/src/render/ui/viewmap.tsx @@ -24,16 +24,8 @@ import { watch } from 'vue'; import { FloorSelector } from '../components'; -import { - ILayerGroupRenderExtends, - FloorDamageExtends, - FloorItemDetail, - LayerGroupAnimate, - LayerGroup, - LayerGroupFloorBinder -} from '../elements'; import { clamp, mean } from 'lodash-es'; -import { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics'; +import { StatisticsDataOneFloor } from './statistics'; import { Tip, TipExpose } from '../components'; import { useKey } from '../use'; import { @@ -61,11 +53,11 @@ const viewMapProps = { export const ViewMap = defineComponent(props => { const nowFloorId = core.status.floorId; - const layerGroupExtends: ILayerGroupRenderExtends[] = [ - new FloorDamageExtends(), - new FloorItemDetail(), - new LayerGroupAnimate() - ]; + // const layerGroupExtends: ILayerGroupRenderExtends[] = [ + // new FloorDamageExtends(), + // new FloorItemDetail(), + // new LayerGroupAnimate() + // ]; const restHeight = STATUS_BAR_HEIGHT - 292; const col = restHeight / 4; @@ -84,7 +76,7 @@ export const ViewMap = defineComponent(props => { }) ); - const group = ref(); + // const group = ref(); const tip = ref(); const statistics = shallowRef(); @@ -107,7 +99,7 @@ export const ViewMap = defineComponent(props => { .realize('@viewMap_down_ten', () => changeFloor(-10)) .realize('@viewMap_book', () => openBook()) .realize('@viewMap_fly', () => fly()) - .realize('@viewMap_reset', () => resetCamera()) + // .realize('@viewMap_reset', () => resetCamera()) .realize('confirm', () => close()) .realize('exit', (_, code, assist) => { // 如果按键不能触发怪物手册,则关闭界面,因为怪物手册和退出默认使用同一个按键,需要特判 @@ -145,10 +137,10 @@ export const ViewMap = defineComponent(props => { else tip.value?.drawTip(`无法飞往${core.floors[id].title}`); }; - const resetCamera = () => { - group.value?.camera.reset(); - group.value?.update(); - }; + // const resetCamera = () => { + // group.value?.camera.reset(); + // group.value?.update(); + // }; //#region 渐变渲染 @@ -195,33 +187,33 @@ export const ViewMap = defineComponent(props => { //#region 地图渲染 - const renderLayer = (floorId: FloorIds) => { - const binder = group.value?.getExtends( - 'floor-binder' - ) as LayerGroupFloorBinder; - binder.bindFloor(floorId); - group.value?.camera.reset(); - core.status.floorId = floorId; - core.status.thisMap = core.status.maps[floorId]; - statistics.value = calculateStatisticsOne(floorId); - }; + // const renderLayer = (floorId: FloorIds) => { + // const binder = group.value?.getExtends( + // 'floor-binder' + // ) as LayerGroupFloorBinder; + // binder.bindFloor(floorId); + // group.value?.camera.reset(); + // core.status.floorId = floorId; + // core.status.thisMap = core.status.maps[floorId]; + // statistics.value = calculateStatisticsOne(floorId); + // }; - const moveCamera = (dx: number, dy: number) => { - const camera = group.value?.camera; - if (!camera) return; - camera.translate(dx / camera.scaleX, dy / camera.scaleX); - group.value?.update(); - }; + // const moveCamera = (dx: number, dy: number) => { + // const camera = group.value?.camera; + // if (!camera) return; + // camera.translate(dx / camera.scaleX, dy / camera.scaleX); + // group.value?.update(); + // }; - const scaleCamera = (scale: number, x: number, y: number) => { - const camera = group.value?.camera; - if (!camera) return; - const [cx, cy] = camera.untransformed(x, y); - camera.translate(cx, cy); - camera.scale(scale); - camera.translate(-cx, -cy); - group.value?.update(); - }; + // const scaleCamera = (scale: number, x: number, y: number) => { + // const camera = group.value?.camera; + // if (!camera) return; + // const [cx, cy] = camera.untransformed(x, y); + // camera.translate(cx, cy); + // camera.scale(scale); + // camera.translate(-cx, -cy); + // group.value?.update(); + // }; //#region 事件监听 @@ -230,7 +222,7 @@ export const ViewMap = defineComponent(props => { if (ev.offsetX < col * 2) { changeFloor(1); } else { - resetCamera(); + // resetCamera(); } }; @@ -293,7 +285,7 @@ export const ViewMap = defineComponent(props => { const dx = ev.offsetX - lastMoveX; const dy = ev.offsetY - lastMoveY; movement += Math.hypot(dx, dy); - moveCamera(dx, dy); + // moveCamera(dx, dy); } moved = true; lastMoveX = ev.offsetX; @@ -322,7 +314,7 @@ export const ViewMap = defineComponent(props => { return; } if (!isFinite(scale) || scale === 0) return; - scaleCamera(scale, cx, cy); + // scaleCamera(scale, cx, cy); } } else { if (mouseDown) { @@ -341,8 +333,8 @@ export const ViewMap = defineComponent(props => { const wheelMap = (ev: IWheelEvent) => { if (ev.altKey) { - const scale = ev.wheelY < 0 ? 1.1 : 0.9; - scaleCamera(scale, ev.offsetX, ev.offsetY); + // const scale = ev.wheelY < 0 ? 1.1 : 0.9; + // scaleCamera(scale, ev.offsetX, ev.offsetY); } else if (ev.ctrlKey) { changeFloor(-Math.sign(ev.wheelY) * 10); } else { @@ -362,7 +354,7 @@ export const ViewMap = defineComponent(props => { }; onMounted(() => { - renderLayer(floorId.value); + // renderLayer(floorId.value); }); onUnmounted(() => { @@ -371,7 +363,7 @@ export const ViewMap = defineComponent(props => { }); watch(floorId, value => { - renderLayer(value); + // renderLayer(value); }); //#region 组件树 @@ -394,7 +386,7 @@ export const ViewMap = defineComponent(props => { v-model:now={now.value} onClose={close} /> - (props => { - + */} (props => { pad={[12, 6]} corner={16} /> - (props => { onLeave={leaveTop} onClick={clickTop} /> - (props => { loc={loc3} anc={[0.5, 0.5]} cursor="pointer" - onClick={resetCamera} + // onClick={resetCamera} /> diff --git a/packages-user/client-modules/src/render/use.ts b/packages-user/client-modules/src/render/use.ts index ea6990e..1fc6484 100644 --- a/packages-user/client-modules/src/render/use.ts +++ b/packages-user/client-modules/src/render/use.ts @@ -1,6 +1,15 @@ +import { + ExcitationCurve, + excited, + IAnimatable, + IExcitableController, + ITransition, + Transition +} from '@motajs/animate'; +import { logger } from '@motajs/common'; +import { IRenderItem, IRenderTreeRoot } from '@motajs/render'; import { Hotkey, gameKey } from '@motajs/system'; import { loading } from '@user/data-base'; -import { TimingFn, Transition } from 'mutate-animate'; import { ComponentInternalInstance, getCurrentInstance, @@ -107,7 +116,7 @@ export interface ITransitionedController { * 设置动画的速率曲线 * @param timing 速率曲线 */ - mode(timing: TimingFn): void; + mode(timing: ExcitationCurve): void; /** * 设置动画的动画时长 @@ -117,37 +126,29 @@ export interface ITransitionedController { } class RenderTransition implements ITransitionedController { - private static key: number = 0; - - private readonly key: string = `$${RenderTransition.key++}`; - public readonly ref: Ref; set value(v: number) { - this.transition.transition(this.key, v); + this.set(v); } get value() { - return this.transition.value[this.key]; + return this.ref.value; } constructor( value: number, - public readonly transition: Transition, + public readonly transition: ITransition, public time: number, - public curve: TimingFn + public curve: ExcitationCurve ) { this.ref = ref(value); - transition.value[this.key] = value; - transition.ticker.add(() => { - this.ref.value = transition.value[this.key]; - }); } set(value: number, time: number = this.time): void { - this.transition.time(time).mode(this.curve).transition(this.key, value); + this.transition.curve(this.curve).transition(this.ref).to(value, time); } - mode(timing: TimingFn): void { + mode(timing: ExcitationCurve): void { this.curve = timing; } @@ -166,6 +167,13 @@ class RenderColorTransition implements ITransitionedController { private readonly keyB: string = `$colorB${RenderColorTransition.key++}`; private readonly keyA: string = `$colorA${RenderColorTransition.key++}`; + private readonly rValue: IAnimatable; + private readonly gValue: IAnimatable; + private readonly bValue: IAnimatable; + private readonly aValue: IAnimatable; + + private readonly controller: IExcitableController | null = null; + public readonly ref: Ref; set value(v: string) { @@ -177,26 +185,32 @@ class RenderColorTransition implements ITransitionedController { constructor( value: string, - public readonly transition: Transition, + public readonly transition: ITransition, public time: number, - public curve: TimingFn + public curve: ExcitationCurve ) { this.ref = ref(value); const [r, g, b, a] = this.decodeColor(value); - transition.value[this.keyR] = r; - transition.value[this.keyG] = g; - transition.value[this.keyB] = b; - transition.value[this.keyA] = a; - transition.ticker.add(() => { - this.ref.value = this.encodeColor(); - }); + this.rValue = { value: r }; + this.gValue = { value: g }; + this.bValue = { value: b }; + this.aValue = { value: a }; + if (!transition.excitation) { + logger.warn(94, 'transitionedColor'); + } else { + this.controller = transition.excitation.add( + excited(() => { + this.ref.value = this.encodeColor(); + }) + ); + } } set(value: string, time: number = this.time): void { this.transitionColor(this.decodeColor(value), time); } - mode(timing: TimingFn): void { + mode(timing: ExcitationCurve): void { this.curve = timing; } @@ -206,12 +220,15 @@ class RenderColorTransition implements ITransitionedController { private transitionColor([r, g, b, a]: ColorRGBA, time: number) { this.transition - .mode(this.curve) - .time(time) - .transition(this.keyR, r) - .transition(this.keyG, g) - .transition(this.keyB, b) - .transition(this.keyA, a); + .curve(this.curve) + .transition(this.rValue) + .to(r, time) + .transition(this.gValue) + .to(g, time) + .transition(this.bValue) + .to(b, time) + .transition(this.aValue) + .to(a, time); } private decodeColor(color: string): ColorRGBA { @@ -272,31 +289,37 @@ class RenderColorTransition implements ITransitionedController { } private encodeColor() { - const r = this.transition.value[this.keyR]; - const g = this.transition.value[this.keyG]; - const b = this.transition.value[this.keyB]; - const a = this.transition.value[this.keyA]; + const r = this.rValue.value; + const g = this.gValue.value; + const b = this.bValue.value; + const a = this.aValue.value; return `rgba(${r},${g},${b},${a})`; } } -const transitionMap = new Map(); +const transitionMap = new Map(); function checkTransition() { const instance = getCurrentInstance(); if (!instance) return null; + const root = instance.root; + if (!root) return null; + const el = root.vnode.el as IRenderItem; + const renderer = el.parent as IRenderTreeRoot; + if (!renderer) return null; if (instance.isUnmounted) { const tran = transitionMap.get(instance); - tran?.ticker.destroy(); + tran?.destroy(); transitionMap.delete(instance); return null; } if (!transitionMap.has(instance)) { const tran = new Transition(); + tran.bindExcitation(renderer.excitation); transitionMap.set(instance, tran); onUnmounted(() => { transitionMap.delete(instance); - tran.ticker.destroy(); + tran.destroy(); }); } const tran = transitionMap.get(instance); @@ -320,7 +343,7 @@ function checkTransition() { export function transitioned( value: number, time: number, - curve: TimingFn + curve: ExcitationCurve ): ITransitionedController | null { const tran = checkTransition(); if (!tran) return null; @@ -344,7 +367,7 @@ export function transitioned( export function transitionedColor( color: string, time: number, - curve: TimingFn + curve: ExcitationCurve ): ITransitionedController | null { const tran = checkTransition(); if (!tran) return null; diff --git a/packages-user/client-modules/src/render/utils/use.ts b/packages-user/client-modules/src/render/utils/use.ts index a5302eb..dbaf210 100644 --- a/packages-user/client-modules/src/render/utils/use.ts +++ b/packages-user/client-modules/src/render/utils/use.ts @@ -1,8 +1,9 @@ import { onUnmounted } from 'vue'; import { WeatherController } from '../weather'; +import { IRenderTreeRoot } from '@motajs/render'; -export function useWeather(): [WeatherController] { - const weather = new WeatherController(); +export function useWeather(renderer: IRenderTreeRoot): [WeatherController] { + const weather = new WeatherController(renderer); onUnmounted(() => { weather.destroy(); diff --git a/packages-user/client-modules/src/render/weather/controller.ts b/packages-user/client-modules/src/render/weather/controller.ts index 8ed8ad7..c5185f7 100644 --- a/packages-user/client-modules/src/render/weather/controller.ts +++ b/packages-user/client-modules/src/render/weather/controller.ts @@ -1,19 +1,21 @@ -import { RenderItem } from '@motajs/render'; +import { IRenderTreeRoot, RenderItem } from '@motajs/render'; import { IWeather, IWeatherController, IWeatherInstance } from './types'; import { logger } from '@motajs/common'; import { isNil } from 'lodash-es'; -import { Ticker } from 'mutate-animate'; +import { IExcitable } from '@motajs/animate'; type WeatherConstructor = new () => IWeather; -export class WeatherController implements IWeatherController { +// todo: refactor? + +export class WeatherController + implements IWeatherController, IExcitable +{ /** 暴露到全局的控制器 */ static extern: Map = new Map(); /** 注册的天气 */ static weathers: Map = new Map(); - private static ticker: Ticker = new Ticker(); - /** 暴露至全局的 id */ private externId?: string; /** 天气元素纵深 */ @@ -23,13 +25,13 @@ export class WeatherController implements IWeatherController { container: RenderItem | null = null; - constructor() { - WeatherController.ticker.add(this.tick); + constructor(readonly renderer: IRenderTreeRoot) { + renderer.delegateExcitable(this); } - private tick = (time: number) => { - this.active.forEach(v => v.weather.tick(time)); - }; + excited(payload: number): void { + this.active.forEach(v => v.weather.tick(payload)); + } /** * 设置天气元素纵深,第一个天气会被设置为 `zIndex`,之后依次是 `zIndex+1` `zIndex+2` ... @@ -111,7 +113,6 @@ export class WeatherController implements IWeatherController { destroy() { this.clearWeather(); - WeatherController.ticker.remove(this.tick); if (!isNil(this.externId)) { WeatherController.extern.delete(this.externId); } @@ -138,8 +139,7 @@ export class WeatherController implements IWeatherController { export class WeatherInstance< R extends RenderItem = RenderItem, T extends IWeather = IWeather -> implements IWeatherInstance -{ +> implements IWeatherInstance { constructor( readonly weather: T, readonly element: R 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 c233836..d029ad1 100644 --- a/packages-user/client-modules/src/render/weather/presets/cloudLike.ts +++ b/packages-user/client-modules/src/render/weather/presets/cloudLike.ts @@ -1,11 +1,11 @@ import { MotaOffscreenCanvas2D, - Sprite, + CustomRenderItem, SizedCanvasImageSource } from '@motajs/render'; import { Weather } from '../weather'; -export abstract class CloudLike extends Weather { +export abstract class CloudLike extends Weather { /** 不透明度 */ private alpha: number = 0; /** 水平速度 */ @@ -78,8 +78,8 @@ export abstract class CloudLike extends Weather { this.cy %= this.image.height; } - createElement(level: number): Sprite { - const element = new Sprite('static', true); + createElement(level: number): CustomRenderItem { + const element = new CustomRenderItem(true); element.setRenderFn(canvas => this.drawImage(canvas)); this.maxSpeed = Math.sqrt(level) * 100; this.vx = ((Math.random() - 0.5) * this.maxSpeed) / 2; diff --git a/packages-user/client-modules/src/render/weather/presets/sun.ts b/packages-user/client-modules/src/render/weather/presets/sun.ts index 23b75ca..5370e8f 100644 --- a/packages-user/client-modules/src/render/weather/presets/sun.ts +++ b/packages-user/client-modules/src/render/weather/presets/sun.ts @@ -1,8 +1,8 @@ -import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render'; +import { MotaOffscreenCanvas2D, CustomRenderItem } from '@motajs/render'; import { Weather } from '../weather'; import { clamp } from 'lodash-es'; -export class SunWeather extends Weather { +export class SunWeather extends Weather { /** 阳光图片 */ private image: HTMLImageElement | null = null; /** 阳光图片的不透明度 */ @@ -41,8 +41,8 @@ export class SunWeather extends Weather { } } - createElement(level: number): Sprite { - const element = new Sprite('static', true); + createElement(level: number): CustomRenderItem { + const element = new CustomRenderItem(true); element.setRenderFn(canvas => this.drawSun(canvas)); this.maxAlpha = level / 10; this.minAlpha = level / 20; diff --git a/packages-user/data-state/src/legacy/move.ts b/packages-user/data-state/src/legacy/move.ts index f2caba0..0f6d89d 100644 --- a/packages-user/data-state/src/legacy/move.ts +++ b/packages-user/data-state/src/legacy/move.ts @@ -2,15 +2,7 @@ import EventEmitter from 'eventemitter3'; import { backDir, toDir } from './utils'; import { loading } from '@user/data-base'; import type { RenderAdapter } from '@motajs/render'; -import type { - FloorLayer, - FloorViewport, - HeroKeyMover, - HeroRenderer, - Layer, - LayerFloorBinder, - LayerMovingRenderable -} from '@user/client-modules'; +import type { HeroKeyMover } from '@user/client-modules'; import { sleep } from '@motajs/common'; import { fromDirectionString, state } from '..'; @@ -235,176 +227,7 @@ export abstract class ObjectMoverBase extends EventEmitter { } } -const enum BlockMoveCode { - Step -} - -export class BlockMover extends ObjectMoverBase { - /** 楼层渲染适配器,用于显示动画 */ - static adapter?: RenderAdapter; - - x: number; - y: number; - floorId: FloorIds; - layer: FloorLayer; - - /** 本次移动中需要进行动画移动的楼层渲染组件 */ - private layerItems: Layer[] = []; - /** 本次移动过程中的移动renderable实例 */ - private renderable?: LayerMovingRenderable; - /** 本次移动的图块id */ - private blockNum: number = 0; - - constructor( - x: number, - y: number, - floorId: FloorIds, - layer: FloorLayer, - dir: Dir = 'down' - ) { - super(); - - this.x = x; - this.y = y; - this.floorId = floorId; - this.moveDir = dir; - this.layer = layer; - } - - /** - * 绑定移动点 - * @param x 绑定点横坐标 - * @param y 绑定点纵坐标 - * @param floorId 绑定点楼层 - * @returns 是否绑定成功,例如如果当前绑定点正在移动,那么就会绑定失败 - */ - bind( - x: number, - y: number, - floorId: FloorIds, - layer: FloorLayer, - dir: Dir = 'down' - ) { - if (this.moving) return false; - this.x = x; - this.y = y; - this.floorId = floorId; - this.moveDir = dir; - this.layer = layer; - return true; - } - - protected async onMoveStart(_controller: IMoveController): Promise { - const adapter = BlockMover.adapter; - if (adapter) { - const list = adapter.items; - const items = [...list].filter(v => { - if (v.layer !== this.layer) return false; - const ex = v.getExtends('floor-binder') as LayerFloorBinder; - if (!ex) return false; - return ex.getFloor() === core.status.floorId; - }); - this.layerItems = items; - } - - let blockNum: number = 0; - if (this.layer === 'event') { - blockNum = core.status.maps[this.floorId].map[this.y][this.x]; - } else { - const array = core.maps._getBgFgMapArray(this.layer, this.floorId); - blockNum = array[this.y][this.x]; - } - this.blockNum = blockNum; - - Mota.r(() => { - const { Layer } = Mota.require('@user/client-modules'); - const r = Layer.getMovingRenderable(blockNum, this.x, this.y); - - if (r) { - this.renderable = r; - this.layerItems.forEach(v => { - v.moving.add(r); - }); - } - }); - - if (this.layer === 'event') { - core.removeBlock(this.x, this.y, this.floorId); - } - } - - protected async onMoveEnd(_controller: IMoveController): Promise { - if (this.renderable) { - this.layerItems.forEach(v => { - v.moving.delete(this.renderable!); - }); - } - - this.layerItems = []; - this.renderable = void 0; - - if (this.layer === 'event') { - core.setBlock(this.blockNum as AllNumbers, this.x, this.y); - } - } - - protected async onStepStart( - step: MoveStepDir, - _controller: IMoveController - ): Promise { - await this.moveAnimate(step); - const { x: dx, y: dy } = core.utils.scan2[this.moveDir]; - this.x += dx; - this.y += dy; - - return BlockMoveCode.Step; - } - - protected async onStepEnd( - _step: MoveStepDir, - _code: BlockMoveCode, - _controller: IMoveController - ): Promise {} - - protected onSetMoveSpeed( - _speed: number, - _controller: IMoveController - ): void {} - - private moveAnimate(_step: MoveStepDir) { - const layer = this.layerItems[0]; - if (!layer) return; - if (!this.renderable) return; - const data = this.renderable; - const fx = this.x; - const fy = this.y; - const { x: dx, y: dy } = core.utils.scan2[this.moveDir]; - const start = Date.now(); - const replay = core.status.replay.speed ?? 1; - const time = replay === 24 ? 1 : this.moveSpeed / replay; - - return new Promise(res => { - layer.delegateTicker( - () => { - const now = Date.now() - start; - const progress = now / time; - data.x = fx + dx * progress; - data.y = fy + dy * progress; - this.layerItems.forEach(v => { - v.update(v); - }); - }, - this.moveSpeed, - () => { - data.x = fx + dx; - data.y = fy + dy; - data.zIndex = fy + dy; - res(); - } - ); - }); - } -} +// todo: refactor interface CanMoveStatus { /** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */ @@ -423,11 +246,6 @@ const enum HeroMoveCode { } export class HeroMover extends ObjectMoverBase { - /** 勇士渲染适配器,用于等待动画等操作 */ - static adapter?: RenderAdapter; - /** 视角适配器 */ - static viewport?: RenderAdapter; - /** 当前移动是否忽略地形 */ private ignoreTerrain: boolean = false; /** 当前移动是否不计入录像 */ @@ -469,8 +287,6 @@ export class HeroMover extends ObjectMoverBase { protected async onMoveStart(controller: IMoveController): Promise { this.beforeMoveSpeed = this.moveSpeed; - const viewport = HeroMover.viewport; - if (!viewport) return; if (!core.isReplaying() || core.status.replay.speed <= 12) { state.hero.startMove(); } @@ -483,21 +299,19 @@ export class HeroMover extends ObjectMoverBase { if (firstDir && firstDir !== 'backward' && firstDir !== 'forward') { const data = this.checkCanMove(x, y, toDir(firstDir as Dir)); if (data.canMove && !data.noPass) { - viewport.sync('startMove'); + // viewport.sync('startMove'); } } } else { - viewport.sync('startMove'); + // viewport.sync('startMove'); } } protected async onMoveEnd(controller: IMoveController): Promise { this.moveSpeed = this.beforeMoveSpeed; this.onSetMoveSpeed(this.moveSpeed, controller); - const viewport = HeroMover.viewport; - if (!viewport) return; await state.hero.endMove(); - viewport.sync('endMove'); + // viewport.sync('endMove'); core.clearContinueAutomaticRoute(); core.stopAutomaticRoute(); } @@ -620,11 +434,11 @@ export class HeroMover extends ObjectMoverBase { _showDir: Dir, moveDir: Dir2 ) { - const viewport = HeroMover.viewport; - if (!viewport) return; + // const viewport = HeroMover.viewport; + // if (!viewport) return; const replay = core.status.replay.speed; const speed = replay === 24 ? 1 : this.moveSpeed / replay; - viewport.all('moveTo', x, y, speed * 1.6); + // viewport.all('moveTo', x, y, speed * 1.6); const replaying = core.isReplaying(); if (replaying) { if (core.status.replay.speed > 12) { @@ -706,13 +520,3 @@ loading.once('coreInit', () => { heroMoveCollection.keyMover = keyMover; }); }); - -// Adapter初始化 -loading.once('coreInit', () => { - if (main.replayChecking || main.mode === 'editor') return; - const Adapter = Mota.require('@motajs/render').RenderAdapter; - const viewport = Adapter.get('viewport'); - const layerAdapter = Adapter.get('layer'); - HeroMover.viewport = viewport; - BlockMover.adapter = layerAdapter; -}); diff --git a/packages-user/legacy-plugin-data/src/fallback.ts b/packages-user/legacy-plugin-data/src/fallback.ts index 5dd540a..b76177e 100644 --- a/packages-user/legacy-plugin-data/src/fallback.ts +++ b/packages-user/legacy-plugin-data/src/fallback.ts @@ -1,7 +1,5 @@ -import type { RenderAdapter } from '@motajs/render'; import type { TimingFn } from 'mutate-animate'; import { - BlockMover, fromDirectionString, heroMoveCollection, MoveStep, @@ -9,38 +7,13 @@ import { } from '@user/data-state'; import { hook, loading } from '@user/data-base'; import { Patch, PatchClass } from '@motajs/legacy-common'; -import type { - LayerDoorAnimate, - LayerGroupAnimate, - FloorViewport, - LayerGroup -} from '@user/client-modules'; import { isNil } from 'lodash-es'; // 向后兼容用,会充当两个版本间过渡的作用 -interface Adapters { - 'door-animate'?: RenderAdapter; - animate?: RenderAdapter; - viewport?: RenderAdapter; -} - -const adapters: Adapters = {}; - export function initFallback() { let fallbackIds: number = 1e8; - if (!main.replayChecking && main.mode === 'play') { - const Adapter = Mota.require('@motajs/render').RenderAdapter; - const doorAnimate = Adapter.get('door-animate'); - const animate = Adapter.get('animate'); - const viewport = Adapter.get('viewport'); - - adapters['door-animate'] = doorAnimate; - adapters['animate'] = animate; - adapters['viewport'] = viewport; - } - const { mover: heroMover } = heroMoveCollection; // ----- 工具函数 @@ -86,8 +59,7 @@ export function initFallback() { Mota.r(() => { // ----- 引入 - const { MotaRenderer: Renderer } = Mota.require('@motajs/render'); - const { Camera } = Mota.require('@user/client-modules'); + const { mainRenderer } = Mota.require('@user/client-modules'); const Animation = Mota.require('MutateAnimate'); const patch = new Patch(PatchClass.Control); @@ -177,7 +149,7 @@ export function initFallback() { noGather?: boolean ) { if (!core.status.hero) return; - // @ts-ignore + // @ts-expect-error todo core.status.hero.loc[name] = value; if (name === 'direction') { const dir = fromDirectionString(value as Dir); @@ -375,9 +347,9 @@ export function initFallback() { function (x: number, y: number, id: AllIds, callback?: () => void) { id = id || ''; if ( - // @ts-ignore + // @ts-expect-error todo (isNil(core.material.icons.animates[id]) && - // @ts-ignore + // @ts-expect-error todo isNil(core.material.icons.npc48[id])) || !isNil(core.getBlock(x, y)) ) { @@ -427,10 +399,10 @@ export function initFallback() { name: AnimationIds, x: number, y: number, - alignWindow?: boolean, + _alignWindow?: boolean, callback?: () => void ) { - // @ts-ignore + // @ts-expect-error todo name = core.getMappedName(name); // 正在播放录像:不显示动画 @@ -444,24 +416,24 @@ export function initFallback() { return -1; } - adapters.animate - ?.all( - 'drawAnimate', - name, - x * 32 + 16, - y * 32 + 16, - alignWindow ?? false - ) - .then(() => { - callback?.(); - }); + // adapters.animate + // ?.all( + // 'drawAnimate', + // name, + // x * 32 + 16, + // y * 32 + 16, + // alignWindow ?? false + // ) + // .then(() => { + // callback?.(); + // }); } ); patch3.add( 'drawHeroAnimate', function (name: AnimationIds, callback?: () => void) { - // @ts-ignore + // @ts-expect-error todo name = core.getMappedName(name); // 正在播放录像或动画不存在:不显示动画 @@ -470,9 +442,9 @@ export function initFallback() { return -1; } - adapters.animate?.global('drawHeroAnimate', name).then(() => { - callback?.(); - }); + // adapters.animate?.global('drawHeroAnimate', name).then(() => { + // callback?.(); + // }); } ); @@ -495,31 +467,31 @@ export function initFallback() { callback?.(); return; } - const mover = new BlockMover( - x, - y, - core.status.floorId, - 'event' - ); - const moveSteps = getMoveSteps(steps); - const resolved = moveSteps.map(v => { - if (v.startsWith('speed')) { - return { type: 'speed', value: Number(v.slice(6)) }; - } else { - return { type: 'dir', value: v as Move2 }; - } - }); - const start: MoveStep = { type: 'speed', value: time }; - mover.insertMove(...[start, ...resolved]); - const controller = mover.startMove(); + // const mover = new BlockMover( + // x, + // y, + // core.status.floorId, + // 'event' + // ); + // const moveSteps = getMoveSteps(steps); + // const resolved = moveSteps.map(v => { + // if (v.startsWith('speed')) { + // return { type: 'speed', value: Number(v.slice(6)) }; + // } else { + // return { type: 'dir', value: v as Move2 }; + // } + // }); + // const start: MoveStep = { type: 'speed', value: time }; + // mover.insertMove(...[start, ...resolved]); + // const controller = mover.startMove(); - if (controller) { - await controller.onEnd; - } + // if (controller) { + // await controller.onEnd; + // } - if (!keep) { - core.removeBlock(mover.x, mover.y); - } + // if (!keep) { + // core.removeBlock(mover.x, mover.y); + // } callback?.(); } ); @@ -584,7 +556,7 @@ export function initFallback() { ) { if (heroMover.moving) return; - adapters.viewport?.all('mutateTo', ex, ey, time); + // adapters.viewport?.all('mutateTo', ex, ey, time); const locked = core.status.lockControl; core.lockControl(); @@ -608,7 +580,7 @@ export function initFallback() { function (destX: number, destY: number, ignoreSteps: number) { const data = core.control.controldata; const success = data.moveDirectly(destX, destY, ignoreSteps); - if (success) adapters.viewport?.all('mutateTo', destX, destY); + // if (success) adapters.viewport?.all('mutateTo', destX, destY); return success; } ); @@ -622,45 +594,7 @@ export function initFallback() { time: number = 1, callback?: () => void ) { - const main = Renderer.get('render-main'); - const layer = main?.getElementById('layer-main') as LayerGroup; - if (!layer) return; - const camera = Camera.for(layer); - camera.clearOperation(); - const translate = camera.addTranslate(); - - const animateTime = - time / Math.max(core.status.replay.speed, 1); - const animate = new Animation.Animation(); - animate - .absolute() - .time(1) - .mode(Animation.linear()) - .move(core.bigmap.offsetX, core.bigmap.offsetY); - animate.time(animateTime).move(x * 32, y * 32); - - camera.applyTranslateAnimation( - translate, - animate, - animateTime + 50 - ); - camera.transform = layer.camera; - - const end = () => { - core.bigmap.offsetX = x * 32; - core.bigmap.offsetY = y * 32; - camera.destroy(); - callback?.(); - }; - - const timeout = window.setTimeout(end, animateTime + 50); - - const id = fallbackIds++; - core.animateFrame.lastAsyncId = id; - core.animateFrame.asyncId[id] = () => { - end(); - clearTimeout(timeout); - }; + // todo } ); }); diff --git a/packages/animate/src/animater.ts b/packages/animate/src/animater.ts index a85a1a2..cbed2bc 100644 --- a/packages/animate/src/animater.ts +++ b/packages/animate/src/animater.ts @@ -67,7 +67,7 @@ export class Animater implements IAnimater { private planning: boolean = false; /** 当前绑定的激励源 */ - private excitation: IExcitation | null = null; + excitation: IExcitation | null = null; /** 当前定义在绑定激励源上的可激励对象 */ private controller: IExcitableController | null = null; @@ -317,6 +317,22 @@ export class Animater implements IAnimater { return index; } + destroy(): void { + this.unbindExcitation(); + this.animatableStatus = null; + this.currentAnimatable = null; + this.planningStore.clear(); + this.groupStore.clear(); + this.whens.clear(); + this.afters.clear(); + this.executingGroupObj = null; + this.executing.clear(); + this.executingMap.clear(); + this.planningStart.clear(); + this.whenBind = null; + this.afterBind = null; + } + //#endregion //#region 动画执行 diff --git a/packages/animate/src/excitation.ts b/packages/animate/src/excitation.ts index 4f25f0a..9bba2d4 100644 --- a/packages/animate/src/excitation.ts +++ b/packages/animate/src/excitation.ts @@ -5,7 +5,8 @@ import { IExcitableController, IExcitationVariator, ExcitationCurve, - VariatorCurveMode + VariatorCurveMode, + IExcitationDivider } from './types'; import { excited } from './utils'; @@ -323,3 +324,63 @@ export class ExcitationVariator super.destroy(); } } + +export class ExcitationDivider + extends ExcitationBase + implements IExcitationDivider +{ + divider: number = 1; + source: IExcitation | null = null; + + /** 当前的激励对象控制器 */ + private controller: IExcitableController | null = null; + /** 当前的激励负载 */ + private nowPayload: T | null = null; + /** 分频计数器 */ + private counter: number = 0; + + payload(): T { + if (!this.source) { + logger.error(52); + throw new Error('Expected an excitation binding'); + } + return this.nowPayload ?? this.source.payload(); + } + + /** + * 激励当前所有的激励源 + * @param payload 激励负载 + */ + excite(payload: T) { + this.counter++; + if (this.counter >= this.divider) { + this.counter = 0; + this.nowPayload = payload; + super.excite(payload); + } + } + + bindExcitation(excitation: IExcitation): void { + this.unbindExcitation(); + this.source = excitation; + this.divider = 1; + this.counter = this.divider - 1; + this.nowPayload = excitation.payload(); + const controller = excitation.add( + excited(payload => this.excite(payload)) + ); + this.controller = controller; + } + + unbindExcitation(): void { + this.controller?.revoke(); + this.counter = 0; + this.divider = 1; + } + + setDivider(divider: number): void { + if (!this.source) return; + this.divider = divider; + this.counter = divider - 1; + } +} diff --git a/packages/animate/src/transition.ts b/packages/animate/src/transition.ts index 1c1c542..adb722d 100644 --- a/packages/animate/src/transition.ts +++ b/packages/animate/src/transition.ts @@ -33,7 +33,7 @@ interface ITransitionData { export class Transition implements ITransition { /** 当前绑定的激励源 */ - private excitation: IExcitation | null = null; + excitation: IExcitation | null = null; /** 当前定义在绑定激励源上的可激励对象 */ private controller: IExcitableController | null = null; @@ -64,7 +64,7 @@ export class Transition implements ITransition { return this; } - transite(animatable: IAnimatable): this { + transition(animatable: IAnimatable): this { this.animatableStatus = animatable; return this; } @@ -115,6 +115,12 @@ export class Transition implements ITransition { this.animatableStatus = null; } + destroy(): void { + this.controller?.revoke(); + this.unbindExcitation(); + this.animatableStatus = null; + } + //#endregion //#region 渐变执行 diff --git a/packages/animate/src/types.ts b/packages/animate/src/types.ts index 2b9580a..d2a2fda 100644 --- a/packages/animate/src/types.ts +++ b/packages/animate/src/types.ts @@ -1,3 +1,5 @@ +//#region 激励对象 + /** * 动画速率曲线,输入时间完成度,输出一个值,时间完成度范围在 `0-1` 之间,而输出值没有范围限制。 * 在不同场景下,速率曲线返回值的含义可能不同,有的可能表示动画完成度,有的可能表示绝对的坐标值。 @@ -43,6 +45,10 @@ export interface IExcitableController { excite(payload: T): void; } +//#endregion + +//#region 激励源 + export interface IExcitation { /** * 获取当前的激励负载 @@ -94,7 +100,7 @@ export interface IExcitationVariator extends IExcitation { readonly source: IExcitation | null; /** - * 绑定激励源对象,变速器将以此激励源为基础实施变速 + * 绑定激励源对象,变速器将以此激励源为基础实施变速。绑定后当前速度值会被设为 1。 * @param excitation 绑定的激励源 */ bindExcitation(excitation: IExcitation): void; @@ -128,6 +134,34 @@ export interface IExcitationVariator extends IExcitation { endAllCurves(): void; } +export interface IExcitationDivider extends IExcitation { + /** 分频器当前的分频比例 */ + readonly divider: number; + /** 当前绑定的激励源 */ + readonly source: IExcitation | null; + + /** + * 绑定激励源对象,分配器将以此激励源为基础实施分配。绑定后分频比例会设为 1。 + * @param excitation 绑定的激励源 + */ + bindExcitation(excitation: IExcitation): void; + + /** + * 取消绑定当前的激励源 + */ + unbindExcitation(): void; + + /** + * 设置当前分频器的分配比例。设置后会在下一次激励源被激励时便触发一次激励,然后按照分配比例触发。 + * @param divider 分配比例 + */ + setDivider(divider: number): void; +} + +//#endregion + +//#region 动画类 + export interface IAnimatable { /** 动画数值 */ value: number; @@ -173,6 +207,9 @@ export const enum EndRelation { } export interface IAnimater extends IExcitable { + /** 渐变对象绑定的激励源 */ + readonly excitation: IExcitation | null; + /** * 在动画执行器上绑定激励源 * @param excitation 绑定的激励源 @@ -310,9 +347,17 @@ export interface IAnimater extends IExcitable { * @param postTime 计划执行后的等待时长,等待这么长时间之后计划才真正结束 */ planEnd(preTime?: number, postTime?: number): number; + + /** + * 销毁此动画对象 + */ + destroy(): void; } export interface ITransition extends IExcitable { + /** 渐变对象绑定的激励源 */ + readonly excitation: IExcitation | null; + /** * 在动画执行器上绑定激励源 * @param excitation 绑定的激励源 @@ -334,7 +379,7 @@ export interface ITransition extends IExcitable { * 绑定渐变对象,之后的渐变操作都会作用在此对象上 * @param animatable 渐变对象 */ - transite(animatable: IAnimatable): this; + transition(animatable: IAnimatable): this; /** * 将当前绑定的值立刻缓慢渐变至目标值 @@ -351,7 +396,14 @@ export interface ITransition extends IExcitable { wait(animatable: IAnimatable): Promise; /** - * 释放当前绑定的渐变对象,防止绑定的渐变对象一直不会被垃圾回收 + * 释放当前使用 {@link transition} 绑定的渐变对象,防止当前被绑定的渐变对象一直不会被垃圾回收。 */ revoke(): void; + + /** + * 销毁当前渐变执行器 + */ + destroy(): void; } + +//#endregion diff --git a/packages/animate/src/utils.ts b/packages/animate/src/utils.ts index cd3c382..43e7a14 100644 --- a/packages/animate/src/utils.ts +++ b/packages/animate/src/utils.ts @@ -250,15 +250,15 @@ export function stackCurve(curves: ExcitationCurve[]): GeneralExcitationCurve { /** * 对速率曲线归一化,此函数假设传入的曲线单调,会使用 `curve(0)` 和 `curve(1)` 作为最大值或最小值。 * - * - `f(0) > f(1)`: `g(x) = (f(x) - f(0)) / (f(0) - f(1))` - * - `f(0) < f(1)`: `g(x) = (f(x) - f(1)) / (f(1) - f(0))` + * - `f(0) > f(1)`: `g(x) = (f(x) - f(1)) / (f(0) - f(1))` + * - `f(0) < f(1)`: `g(x) = (f(x) - f(0)) / (f(1) - f(0))` * - `f(0) = f(1)`: `g(x) = f(x)` * @param curve 需要归一化的曲线 * @returns */ export function normalize(curve: ExcitationCurve): ExcitationCurve { - const head = curve(1); - const tail = curve(0); + const head = curve(0); + const tail = curve(1); if (head > tail) { const diff = head - tail; return p => (curve(p) - tail) / diff; diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index c72445f..b143a1e 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -13,8 +13,8 @@ "11": "Cache depth cannot larger than 31.", "12": "Cannot move while status is not 'moving'. Call 'readyMove' first.", "13": "Cannot compile $1 shader. Error info: $2", - "14": "", - "15": "", + "14": "Cannot register render tag, since the tag name has been used.", + "15": "Render tag '$1' not registed.", "16": "Cannot find log message for $1 code $2.", "17": "Cannot use shader program for shader element that does not belong to it.", "18": "Cannot delete shader program for shader element that does not belong to it.", @@ -51,6 +51,7 @@ "49": "Cannot $1 on variator without excitation binding.", "50": "Expected a planEnd call after animation plan calling.", "51": "Animatable object cannot be animated by plans with the same start time.", + "52": "To get divider payload, an excitation binding is expected.", "1201": "Floor-damage extension needs 'floor-binder' extension as dependency." }, "warn": { @@ -147,6 +148,7 @@ "91": "Cannot add follower, since specified follower number $1 does not exist.", "92": "Followers can only be added when the last follower is not moving.", "93": "Followers can only be removed when the last follower is not moving.", + "94": "Expecting an excitation binding when using '$1'", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." } } diff --git a/packages/render-vue/src/elements.ts b/packages/render-vue/src/elements.ts new file mode 100644 index 0000000..39e0c20 --- /dev/null +++ b/packages/render-vue/src/elements.ts @@ -0,0 +1,64 @@ +import { ReservedProps } from 'vue'; +import EventEmitter from 'eventemitter3'; +import { + BezierProps, + CirclesProps, + CommentProps, + ConatinerCustomProps, + ContainerProps, + CustomProps, + EllipseProps, + ImageProps, + LineProps, + PathProps, + QuadraticProps, + RectProps, + RectRProps, + ShaderProps, + TextProps +} from './props'; +import { ERenderItemEvent } from '@motajs/render'; + +export type WrapEventEmitterEvents = + T extends string | symbol + ? T + : { + [P in keyof T]: T[P] extends any[] + ? (...args: T[P]) => void + : (...args: any[]) => void; + }; + +type MappingEvent = { + [P in keyof WrapEventEmitterEvents as P extends string + ? `on${Capitalize

}` + : never]?: WrapEventEmitterEvents[P]; +}; + +export type TagDefine = T & + MappingEvent & + ReservedProps; + +declare module 'vue/jsx-runtime' { + namespace JSX { + export interface IntrinsicElements { + container: TagDefine; + 'container-custom': TagDefine< + ConatinerCustomProps, + ERenderItemEvent + >; + shader: TagDefine; + text: TagDefine; + image: TagDefine; + comment: TagDefine; + custom: TagDefine; + 'g-rect': TagDefine; + 'g-circle': TagDefine; + 'g-ellipse': TagDefine; + 'g-line': TagDefine; + 'g-bezier': TagDefine; + 'g-quad': TagDefine; + 'g-path': TagDefine; + 'g-rectr': TagDefine; + } + } +} diff --git a/packages/render-vue/src/elements.tsx b/packages/render-vue/src/elements.tsx deleted file mode 100644 index c721e12..0000000 --- a/packages/render-vue/src/elements.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { - ComponentOptionsMixin, - defineComponent, - DefineComponent, - h, - ReservedProps, - VNodeProps -} from 'vue'; -import EventEmitter from 'eventemitter3'; -import { - BaseProps, - BezierProps, - CirclesProps, - CommentProps, - ConatinerCustomProps, - ContainerProps, - CustomProps, - EllipseProps, - ImageProps, - LineProps, - PathProps, - QuadraticProps, - RectProps, - RectRProps, - ShaderProps, - SpriteProps, - TextProps -} from './props'; -import { - ERenderItemEvent, - RenderItem, - ESpriteEvent, - EContainerEvent, - EShaderEvent, - EImageEvent, - ETextEvent, - EGraphicItemEvent -} from '@motajs/render'; - -export type WrapEventEmitterEvents = - T extends string | symbol - ? T - : { - [P in keyof T]: T[P] extends any[] - ? (...args: T[P]) => void - : (...args: any[]) => void; - }; - -type MappingEvent = { - [P in keyof WrapEventEmitterEvents as P extends string - ? `on${Capitalize

}` - : never]?: WrapEventEmitterEvents[P]; -}; - -type _Define

= DefineComponent< - P, - {}, - {}, - {}, - {}, - ComponentOptionsMixin, - ComponentOptionsMixin, - WrapEventEmitterEvents, - Exclude, number | symbol>, - VNodeProps, - Readonly

> ->; - -export type TagDefine = T & - MappingEvent & - ReservedProps; - -declare module 'vue/jsx-runtime' { - namespace JSX { - export interface IntrinsicElements { - sprite: TagDefine; - container: TagDefine; - 'container-custom': TagDefine< - ConatinerCustomProps, - EContainerEvent - >; - shader: TagDefine; - text: TagDefine; - image: TagDefine; - comment: TagDefine; - custom: TagDefine; - 'g-rect': TagDefine; - 'g-circle': TagDefine; - 'g-ellipse': TagDefine; - 'g-line': TagDefine; - 'g-bezier': TagDefine; - 'g-quad': TagDefine; - 'g-path': TagDefine; - 'g-rectr': TagDefine; - } - } -} - -export interface InstancedElementProp { - item: RenderItem; -} - -export function wrapInstancedComponent< - P extends BaseProps = BaseProps, - E extends ERenderItemEvent = ERenderItemEvent, - C extends RenderItem = RenderItem ->(onCreate: (props: P) => C): _Define { - const Com = defineComponent((props, ctx) => { - return () => { - const p = { - ...props, - ...ctx.attrs, - _item: onCreate - }; - return h('custom', p, ctx.slots); - }; - }); - return Com as _Define; -} diff --git a/packages/render-vue/src/index.ts b/packages/render-vue/src/index.ts index c975a12..aae535f 100644 --- a/packages/render-vue/src/index.ts +++ b/packages/render-vue/src/index.ts @@ -1,14 +1,6 @@ -import { ERenderItemEvent } from '@motajs/render'; -import { TagDefine } from './elements'; -import { BaseProps } from './props'; - -export type DefaultProps< - P extends BaseProps = BaseProps, - E extends ERenderItemEvent = ERenderItemEvent -> = TagDefine; - export * from './elements'; -export * from './map'; export * from './props'; export * from './renderer'; +export * from './tag'; +export * from './types'; export * from './use'; diff --git a/packages/render-vue/src/map.ts b/packages/render-vue/src/map.ts deleted file mode 100644 index 5be1d33..0000000 --- a/packages/render-vue/src/map.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { ElementNamespace, VNodeProps } from 'vue'; -import { logger } from '@motajs/common'; -import { - ERenderItemEvent, - RenderItem, - RenderItemPosition, - Container, - ContainerCustom, - MotaRenderer, - Sprite, - Shader, - Comment, - ETextEvent, - Image, - Text, - BezierCurve, - Circle, - Ellipse, - Line, - Path, - QuadraticCurve, - Rect, - RectR -} from '@motajs/render'; - -type OnItemCreate< - E extends ERenderItemEvent = ERenderItemEvent, - T extends RenderItem = RenderItem -> = ( - namespace?: ElementNamespace, - isCustomizedBuiltIn?: string, - vnodeProps?: (VNodeProps & { [key: string]: any }) | null -) => T; - -class RenderTagMap { - private map: Map = new Map(); - - /** - * 注册一个标签,每个标签对应一类元素,重复注册会覆盖之前的 - * @param tag 标签名称 - * @param ele 对应的元素类或其构造器 - */ - register>( - tag: string, - onCreate: OnItemCreate - ) { - if (this.map.has(tag)) { - logger.warn(34, tag); - } - this.map.set(tag, onCreate); - } - - /** - * 获取一个标签对应的元素构造器 - * @param tag 标签名 - */ - get>( - tag: string - ): OnItemCreate | undefined { - return this.map.get(tag) as OnItemCreate; - } -} - -export const tagMap = new RenderTagMap(); - -export const standardElement = ( - Item: new ( - type: RenderItemPosition, - cache?: boolean, - fall?: boolean - ) => RenderItem -) => { - return (_0: any, _1: any, props?: any) => { - if (!props) return new Item('static'); - else { - const { - type = 'static', - cache = true, - fall = false, - nocache = false - } = props; - return new Item(type, cache && !nocache, fall); - } - }; -}; - -export const standardElementNoCache = ( - Item: new ( - type: RenderItemPosition, - cache?: boolean, - fall?: boolean - ) => RenderItem -) => { - return (_0: any, _1: any, props?: any) => { - if (!props) return new Item('static'); - else { - const { - type = 'static', - cache = false, - fall = false, - nocache = true - } = props; - return new Item(type, cache && !nocache, fall); - } - }; -}; - -const enum ElementState { - None = 0, - Cache = 1, - Fall = 2 -} - -/** - * standardElementFor - */ -const _se = ( - Item: new ( - type: RenderItemPosition, - cache?: boolean, - fall?: boolean - ) => RenderItem, - position: RenderItemPosition, - state: ElementState -) => { - const defaultCache = !!(state & ElementState.Cache); - const defautFall = !!(state & ElementState.Fall); - - return (_0: any, _1: any, props?: any) => { - if (!props) return new Item('absolute'); - else { - const { - type = position, - cache = defaultCache, - fall = defautFall, - nocache = !defaultCache - } = props; - return new Item(type, cache && !nocache, fall); - } - }; -}; - -// Default elements -tagMap.register('container', standardElement(Container)); -tagMap.register('container-custom', standardElement(ContainerCustom)); -tagMap.register('template', standardElement(Container)); -tagMap.register('mota-renderer', (_0, _1, props) => { - return new MotaRenderer(props?.id); -}); -tagMap.register('sprite', standardElement(Sprite)); -tagMap.register('text', (_0, _1, props) => { - if (!props) return new Text(); - else { - const { type = 'static', text = '' } = props; - return new Text(text, type); - } -}); -const emptyImage = document.createElement('canvas'); -emptyImage.width = 1; -emptyImage.height = 1; -tagMap.register('image', (_0, _1, props) => { - if (!props) return new Image(emptyImage); - else { - const { image = emptyImage, type = 'static' } = props; - return new Image(image, type); - } -}); -tagMap.register('comment', (_0, _1, props) => { - if (!props) return new Comment(); - else { - const { text = '' } = props; - return new Comment(text); - } -}); -tagMap.register('shader', (_0, _1, props) => { - if (!props) return new Shader(); - else { - const { type = 'static' } = props; - return new Shader(type); - } -}); -tagMap.register('custom', (_0, _1, props) => { - if (!props) { - logger.error(22); - throw new Error('Cannot create custom element.'); - } else { - const item = props._item; - if (!item) { - logger.error(22); - throw new Error('Cannot create custom element.'); - } - return item(props); - } -}); -tagMap.register('g-rect', standardElementNoCache(Rect)); -tagMap.register('g-circle', standardElementNoCache(Circle)); -tagMap.register('g-ellipse', standardElementNoCache(Ellipse)); -tagMap.register('g-line', standardElementNoCache(Line)); -tagMap.register('g-bezier', standardElementNoCache(BezierCurve)); -tagMap.register('g-quad', standardElementNoCache(QuadraticCurve)); -tagMap.register('g-path', standardElementNoCache(Path)); -tagMap.register('g-rectr', standardElementNoCache(RectR)); diff --git a/packages/render-vue/src/props.ts b/packages/render-vue/src/props.ts index 6c57305..d52e4f1 100644 --- a/packages/render-vue/src/props.ts +++ b/packages/render-vue/src/props.ts @@ -1,29 +1,24 @@ import { - RenderFunction, - RenderItem, - RenderItemPosition, Transform, ElementAnchor, ElementLocator, ElementScale, + CanvasStyle, + Font, + RenderPosition, + CustomRenderFunction, CustomContainerRenderFn, CustomContainerPropagateFn, - CanvasStyle, + ILineProperty, BezierParams, CircleParams, EllipseParams, - ILineProperty, LineParams, QuadParams, RectRCircleParams, - RectREllipseParams, - Font + RectREllipseParams } from '@motajs/render'; -export interface CustomProps { - _item: (props: BaseProps) => RenderItem; -} - export interface BaseProps { /** 元素的横坐标 */ x?: number; @@ -51,8 +46,8 @@ export interface BaseProps { hidden?: boolean; /** 元素的变换矩阵 */ transform?: Transform; - /** 元素的定位模式,static 表示常规定位,absolute 定位模式下元素位置始终处于左上角 */ - type?: RenderItemPosition; + /** 元素的定位模式 */ + type?: RenderPosition; /** 是否启用缓存,用处较少,主要用于一些默认不启用缓存的元素的特殊优化 */ cache?: boolean; /** 是否不启用缓存,优先级大于 cache,用处较少,主要用于一些特殊优化 */ @@ -83,9 +78,9 @@ export interface BaseProps { noevent?: boolean; } -export interface SpriteProps extends BaseProps { +export interface CustomProps extends BaseProps { /** 自定义的渲染函数 */ - render?: RenderFunction; + render?: CustomRenderFunction; } export interface ContainerProps extends BaseProps {} diff --git a/packages/render-vue/src/renderer.ts b/packages/render-vue/src/renderer.ts index e9ebecb..ffdfe94 100644 --- a/packages/render-vue/src/renderer.ts +++ b/packages/render-vue/src/renderer.ts @@ -1,103 +1,114 @@ import { logger } from '@motajs/common'; import { - ERenderItemEvent, RenderItem, - ETextEvent, Text, - Comment + Comment, + IRenderItem, + IRenderTreeRoot } from '@motajs/render'; import { ComponentInternalInstance, + CreateAppFunction, createRenderer, ElementNamespace, + RootRenderFunction, VNodeProps } from 'vue'; -import { tagMap } from './map'; +import { IRenderTagManager } from './types'; +import { RenderTagManager } from './tag'; -export const { createApp, render } = createRenderer({ - patchProp: function ( - el: RenderItem, - key: string, - prevValue: any, - nextValue: any, - namespace?: ElementNamespace, - parentComponent?: ComponentInternalInstance | null - ): void { - el.patchProp(key, prevValue, nextValue, namespace, parentComponent); - }, +export interface RendererData { + render: RootRenderFunction; + createApp: CreateAppFunction; + tagManager: IRenderTagManager; +} - insert: function ( - el: RenderItem, - parent: RenderItem, - _anchor?: RenderItem | null - ): void { - parent.appendChild(el); - }, +export function createRendererFor(renderer: IRenderTreeRoot) { + const tagManager = new RenderTagManager(renderer); - remove: function (el: RenderItem): void { - el.destroy(); - }, + const { createApp, render } = createRenderer({ + patchProp: function ( + el: RenderItem, + key: string, + prevValue: any, + nextValue: any, + namespace?: ElementNamespace, + parentComponent?: ComponentInternalInstance | null + ): void { + el.patchProp(key, prevValue, nextValue, namespace, parentComponent); + }, - createElement: function ( - type: string, - namespace?: ElementNamespace, - isCustomizedBuiltIn?: string, - vnodeProps?: (VNodeProps & { [key: string]: any }) | null - ): RenderItem { - const onCreate = tagMap.get(type); - if (!onCreate) { - logger.error(20, type); - throw new Error(`Cannot create element '${type}'`); + insert: function ( + el: IRenderItem, + parent: RenderItem, + _anchor?: IRenderItem | null + ): void { + parent.appendChild(el); + }, + + remove: function (el: IRenderItem): void { + el.destroy(); + }, + + createElement: function ( + type: string, + _namespace?: ElementNamespace, + _isCustomizedBuiltIn?: string, + vnodeProps?: (VNodeProps & { [key: string]: any }) | null + ): IRenderItem { + const tag = tagManager.getTag(type); + if (!tag) { + logger.error(20, type); + throw new Error(`Cannot create element '${type}'`); + } + return tag.onCreate(vnodeProps); + }, + + createText: function (text: string): IRenderItem { + if (/^\s*$/.test(text)) { + return new Comment(); + } else { + logger.warn(38); + } + return new Text(text); + }, + + createComment: function (text: string): IRenderItem { + return renderer.createElement('comment', text); + }, + + setText: function (node: IRenderItem, text: string): void { + if (node instanceof Text) { + node.setText(text); + } else { + logger.warn(39); + } + }, + + setElementText: function (node: IRenderItem, text: string): void { + if (node instanceof Text) { + node.setText(text); + } else { + logger.warn(39); + } + }, + + parentNode: function (node: IRenderItem): IRenderItem | null { + return node.parent ?? null; + }, + + nextSibling: function (node: IRenderItem): IRenderItem | null { + if (!node) return null; + if (!node.parent) { + return null; + } else { + const parent = node.parent; + const list = [...parent.children]; + const index = list.indexOf(node); + return list[index] ?? null; + } } - return onCreate(namespace, isCustomizedBuiltIn, vnodeProps); - }, + }); - createText: function (text: string): RenderItem { - if (/^\s*$/.test(text)) { - return new Comment(); - } else { - logger.warn(38); - } - return new Text(text); - }, - - createComment: function (text: string): RenderItem { - return new Comment(text); - }, - - setText: function (node: RenderItem, text: string): void { - if (node instanceof Text) { - node.setText(text); - } else { - logger.warn(39); - } - }, - - setElementText: function (node: RenderItem, text: string): void { - if (node instanceof Text) { - node.setText(text); - } else { - logger.warn(39); - } - }, - - parentNode: function ( - node: RenderItem - ): RenderItem | null { - return node.parent ?? null; - }, - - nextSibling: function ( - node: RenderItem - ): RenderItem | null { - if (!node) return null; - if (!node.parent) { - return null; - } else { - const parent = node.parent; - const list = [...parent.children]; - const index = list.indexOf(node); - return list[index] ?? null; - } - } -}); + return { tagManager, createApp, render }; +} diff --git a/packages/render-vue/src/tag.ts b/packages/render-vue/src/tag.ts new file mode 100644 index 0000000..33258b4 --- /dev/null +++ b/packages/render-vue/src/tag.ts @@ -0,0 +1,128 @@ +import { + BezierCurve, + Circle, + Comment, + Container, + CustomContainer, + CustomRenderItem, + Ellipse, + Image, + IRenderItem, + IRenderTreeRoot, + Line, + Path, + QuadraticCurve, + Rect, + RectR, + Shader, + Text +} from '@motajs/render'; +import { IRenderTagInfo, IRenderTagManager, TagCreateFunction } from './types'; +import { logger } from '@motajs/common'; + +export class RenderTagManager implements IRenderTagManager { + /** 标签注册映射 */ + private readonly tagRegistry: Map = new Map(); + /** 空图片 */ + private readonly emptyImg: HTMLCanvasElement; + + constructor(readonly renderer: IRenderTreeRoot) { + const emptyImage = document.createElement('canvas'); + emptyImage.width = 1; + emptyImage.height = 1; + this.emptyImg = emptyImage; + + this.resgiterIntrinsicTags(); + } + + /** + * 注册所有的内置标签 + */ + private resgiterIntrinsicTags() { + this.registerTag( + 'container', + this.createStandardElement(true, Container) + ); + this.registerTag( + 'custom', + this.createStandardElement(true, CustomRenderItem) + ); + this.registerTag('text', props => { + if (!props) return this.renderer.createElement(Text, '', false); + const { text = '', nocache = true, cache = false } = props; + return this.renderer.createElement(Text, text, cache && !nocache); + }); + this.registerTag('image', props => { + if (!props) { + return this.renderer.createElement(Image, this.emptyImg, false); + } + const { + image = this.emptyImg, + nocache = true, + cache = false + } = props; + return this.renderer.createElement(Image, image, cache && !nocache); + }); + this.registerTag('shader', this.createNoParamElement(Shader)); + this.registerTag('comment', props => { + if (!props) return this.renderer.createElement(Comment); + else return this.renderer.createElement(Comment, props.text ?? ''); + }); + this.registerTag( + 'template', + this.createStandardElement(false, Container) + ); + this.registerTag( + 'custom-container', + this.createStandardElement(true, CustomContainer) + ); + this.registerTag('g-rect', this.createStandardElement(false, Rect)); + this.registerTag('g-circle', this.createStandardElement(false, Circle)); + this.registerTag( + 'g-ellipse', + this.createStandardElement(false, Ellipse) + ); + this.registerTag('g-line', this.createStandardElement(false, Line)); + this.registerTag( + 'g-bezier', + this.createStandardElement(false, BezierCurve) + ); + this.registerTag( + 'g-quad', + this.createStandardElement(false, QuadraticCurve) + ); + this.registerTag('g-path', this.createStandardElement(false, Path)); + this.registerTag('g-rectr', this.createStandardElement(false, RectR)); + } + + registerTag(tag: string, onCreate: TagCreateFunction): void { + if (this.tagRegistry.has(tag)) { + logger.error(14, tag); + return; + } + const info: IRenderTagInfo = { onCreate }; + this.tagRegistry.set(tag, info); + } + + getTag(tag: string): IRenderTagInfo | null { + return this.tagRegistry.get(tag) ?? null; + } + + createStandardElement( + cache: boolean, + Cons: new (enableCache?: boolean) => IRenderItem + ): TagCreateFunction { + const enable = cache; + return props => { + if (!props) { + return this.renderer.createElement(Cons, enable); + } + const { nocache = !enable, cache = enable } = props; + return this.renderer.createElement(Cons, cache && !nocache); + }; + } + + createNoParamElement(Cons: new () => IRenderItem): TagCreateFunction { + return () => this.renderer.createElement(Cons); + } +} diff --git a/packages/render-vue/src/types.ts b/packages/render-vue/src/types.ts new file mode 100644 index 0000000..a90549e --- /dev/null +++ b/packages/render-vue/src/types.ts @@ -0,0 +1,134 @@ +import { ERenderItemEvent, IRenderItem, IRenderTreeRoot } from '@motajs/render'; +import { VNodeProps } from 'vue'; +import { BaseProps } from './props'; +import { TagDefine } from './elements'; +import { + IAnimater, + IExcitable, + IExcitation, + ITransition +} from '@motajs/animate'; +import EventEmitter from 'eventemitter3'; + +//#region 标签管理 + +export type TagCreateFunction = ( + props?: (VNodeProps & { [key: string]: any }) | null +) => IRenderItem; + +export interface IRenderTagInfo { + /** 标签创建函数 */ + readonly onCreate: TagCreateFunction; +} + +export interface IRenderTagManager { + /** 标签管理器对应的渲染根元素 */ + readonly renderer: IRenderTreeRoot; + + /** + * 注册自定义标签 + * @param tag 标签名称 + * @param onCreate 创建标签的函数 + */ + registerTag(tag: string, onCreate: TagCreateFunction): void; + + /** + * 获取指定标签的信息 + * @param tag 标签名称 + */ + getTag(tag: string): IRenderTagInfo | null; + + /** + * 创建常规元素的创建函数 + * @param cache 创建时默认是否启用缓存 + * @param Cons 渲染元素构造器 + */ + createStandardElement( + cache: boolean, + Cons: new (enableCache?: boolean) => IRenderItem + ): TagCreateFunction; + + /** + * 创建无创建参数的元素创建函数 + * @param Cons 渲染元素构造器 + */ + createNoParamElement(Cons: new () => IRenderItem): TagCreateFunction; +} + +export type DefaultProps< + P extends BaseProps = BaseProps, + E extends ERenderItemEvent = ERenderItemEvent +> = TagDefine; + +//#endregion + +//#region 功能接口 + +export interface IRendererUsing { + /** Using 对象使用的渲染器 */ + readonly renderer: IRenderTreeRoot; + + /** + * 当渲染器的激励源触发激励时同时执行传入的可激励对象,相当于每帧执行一次。 + * + * 在**组件内**需要每帧执行时**务必**使用此接口,否则可能会导致激励对象没有被正确删除,出现内存泄漏问题。 + * + * 在**组件外不要**使用此接口,否则可能没有效果! + * @param excitable 可激励对象 + */ + onExcited(excitable: IExcitable): void; + + /** + * 当渲染器的激励源触发激励时同时执行传入的函数,相当于每帧执行一次。 + * + * 在**组件内**需要每帧执行时**务必**使用此接口,否则可能会导致激励对象没有被正确删除,出现内存泄漏问题。 + * + * 在**组件外不要**使用此接口,否则可能没有效果! + * @param excitable 可激励对象 + */ + onExcitedFunc(fn: (payload: number) => void): void; + + /** + * 监听渲染元素的指定事件。 + * + * 在组件内如果你需要监听事件,应该先考虑使用 `JSX` 的 `onXxx` 来监听事件,而不是使用此接口。 + * + * 如果 `JSX` 的 `onXxx` 不能满足你的需求,再考虑使用此接口,在组件内**不要**直接使用 `item.on` 来监听事件, + * 否则可能导致事件监听没有正确删除,出现内存泄漏问题。 + * + * 在**组件外不要**使用此接口,否则可能没有效果! + * @param item 要监听的渲染元素 + * @param key 事件类型 + * @param listener 当事件触发时执行的函数 + */ + listenEvent< + T extends ERenderItemEvent, + K extends EventEmitter.EventNames + >( + item: IRenderItem, + key: K, + listener: EventEmitter.EventListener + ): void; + + /** + * 创建一个动画执行器。 + * + * 在**组件内**需要动画器时**务必**使用此接口,否则可能会动画器没有被正确销毁,出现内存泄漏问题。 + * + * 在**组件外不要**使用此接口,否则可能没有效果! + * @param excitation 动画执行器使用的激励源 + */ + useAnimater(excitation: IExcitation): IAnimater; + + /** + * 创建一个渐变执行器。 + * + * 在**组件内**需要渐变对象时**务必**使用此接口,否则可能会渐变对象没有被正确销毁,出现内存泄漏问题。 + * + * 在**组件外不要**使用此接口,否则可能没有效果! + * @param excitation 渐变执行器使用的激励源 + */ + useTransition(excitation: IExcitation): ITransition; +} + +//#endregion diff --git a/packages/render-vue/src/use.ts b/packages/render-vue/src/use.ts index 157fde6..b23674c 100644 --- a/packages/render-vue/src/use.ts +++ b/packages/render-vue/src/use.ts @@ -1,54 +1,66 @@ -import { Animation, Ticker, Transition } from 'mutate-animate'; -import { ERenderItemEvent, RenderItem } from '@motajs/render'; +import { ERenderItemEvent, IRenderItem, IRenderTreeRoot } from '@motajs/render'; import { onMounted, onUnmounted } from 'vue'; import EventEmitter from 'eventemitter3'; +import { IRendererUsing } from './types'; +import { + IExcitable, + IAnimater, + ITransition, + Animater, + IExcitation, + Transition, + excited +} from '@motajs/animate'; -const ticker = new Ticker(); +export class RendererUsing implements IRendererUsing { + constructor(readonly renderer: IRenderTreeRoot) {} -/** - * 在组件中每帧执行一次函数 - * @param fn 每帧执行的函数 - */ -export function onTick(fn: (time: number) => void) { - onMounted(() => { - ticker.add(fn); - }); - onUnmounted(() => { - ticker.remove(fn); - }); -} - -type AnimationUsing = [Animation]; -type TransitionUsing = [Transition]; - -/** - * 在组件中创建一个动画实例 - */ -export function useAnimation(): AnimationUsing { - const ani = new Animation(); - onUnmounted(() => { - ani.ticker.destroy(); - }); - return [ani]; -} - -/** - * 在组件中创建一个渐变实例 - */ -export function useTransition(): TransitionUsing { - const tran = new Transition(); - onUnmounted(() => { - tran.ticker.destroy(); - }); - return [tran]; -} - -export function onEvent< - T extends ERenderItemEvent, - K extends EventEmitter.EventNames ->(item: RenderItem, key: K, listener: EventEmitter.EventListener) { - item.on(key, listener); - onUnmounted(() => { - item.off(key, listener); - }); + onExcited(excitable: IExcitable): void { + onMounted(() => { + this.renderer.excitation.add(excitable); + }); + onUnmounted(() => { + this.renderer.excitation.remove(excitable); + }); + } + + onExcitedFunc(fn: (payload: number) => void): void { + this.onExcited(excited(fn)); + } + + listenEvent< + T extends ERenderItemEvent, + K extends EventEmitter.EventNames + >( + item: IRenderItem, + key: K, + listener: EventEmitter.EventListener + ): void { + item.on(key, listener); + onUnmounted(() => { + item.off(key, listener); + }); + } + + useAnimater(excitation: IExcitation): IAnimater { + const anim = new Animater(); + onMounted(() => { + anim.bindExcitation(excitation); + }); + onUnmounted(() => { + anim.unbindExcitation(); + }); + return anim; + } + + useTransition(excitation: IExcitation): ITransition { + const tran = new Transition(); + onMounted(() => { + tran.bindExcitation(excitation); + }); + onUnmounted(() => { + tran.unbindExcitation(); + }); + return tran; + } } diff --git a/packages/render/src/core/container.ts b/packages/render/src/core/container.ts index df0f186..e01b512 100644 --- a/packages/render/src/core/container.ts +++ b/packages/render/src/core/container.ts @@ -1,44 +1,29 @@ import { MotaOffscreenCanvas2D } from './canvas2d'; import { ActionType, EventProgress, ActionEventMap } from './event'; -import { - ERenderItemEvent, - IRenderChildable, - RenderItem, - RenderItemPosition -} from './item'; +import { RenderItem } from './item'; import { Transform } from './transform'; +import { + CustomContainerPropagateFn, + CustomContainerRenderFn, + IRenderItem +} from './types'; -export interface EContainerEvent extends ERenderItemEvent {} - -export class Container - extends RenderItem - implements IRenderChildable -{ +export class Container extends RenderItem { 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 + _transform: Transform ): void { + if (this.needSort) { + this.sortChildren(); + this.needSort = false; + } this.sortedChildren.forEach(v => { if (v.hidden) return; - v.renderContent(canvas, transform); + v.renderContent(canvas); }); } @@ -48,28 +33,22 @@ export class Container } requestSort() { - if (!this.needSort) { - this.needSort = true; - this.requestBeforeFrame(() => { - this.needSort = false; - this.sortChildren(); - }); - } + this.needSort = true; } /** * 添加子元素到这个容器上,然后在下一个tick执行更新 * @param children 要添加的子元素 */ - appendChild(...children: RenderItem[]) { + appendChild(...children: IRenderItem[]) { children.forEach(v => { v.appendTo(this); }); this.requestSort(); - this.update(this); + this.update(); } - removeChild(...child: RenderItem[]): void { + removeChild(...child: IRenderItem[]): void { let changed = false; child.forEach(v => { if (v.parent !== this) return; @@ -79,7 +58,7 @@ export class Container } }); if (changed) this.requestSort(); - this.update(this); + this.update(); } appendTo(parent: RenderItem): void { @@ -96,7 +75,7 @@ export class Container * 遍历这个元素中的每个子元素,并执行传入的函数 * @param fn 对每个元素执行的函数 */ - forEachChild(fn: (ele: RenderItem) => void) { + protected forEachChild(fn: (ele: RenderItem) => void) { const stack: RenderItem[] = [this]; while (stack.length > 0) { const ele = stack.pop()!; @@ -109,7 +88,6 @@ export class Container this.sortedChildren = [...this.children] .filter(v => !v.isComment) .sort((a, b) => a.zIndex - b.zIndex); - this.update(); } protected propagateEvent( @@ -145,27 +123,7 @@ export class Container } } -export type CustomContainerRenderFn = ( - canvas: MotaOffscreenCanvas2D, - children: RenderItem[], - transform: Transform -) => void; - -export type CustomContainerPropagateOrigin = ( - type: T, - progress: EventProgress, - event: ActionEventMap[T] -) => void; - -export type CustomContainerPropagateFn = ( - type: T, - progress: EventProgress, - event: ActionEventMap[T], - container: ContainerCustom, - origin: CustomContainerPropagateOrigin -) => void; - -export class ContainerCustom extends Container { +export class CustomContainer extends Container { private renderFn?: CustomContainerRenderFn; private propagateFn?: CustomContainerPropagateFn; diff --git a/packages/render/src/core/custom.ts b/packages/render/src/core/custom.ts new file mode 100644 index 0000000..4401636 --- /dev/null +++ b/packages/render/src/core/custom.ts @@ -0,0 +1,60 @@ +import { RenderItem } from './item'; +import { MotaOffscreenCanvas2D } from './canvas2d'; +import { CustomRenderFunction } from './types'; +import { Ref } from 'vue'; + +export class CustomRenderItem extends RenderItem { + renderFn: CustomRenderFunction; + + /** + * 创建一个精灵,可以自由在上面渲染内容 + * @param type 渲染模式,absolute表示绝对位置,不会跟随自身的Transform改变 + * @param cache 是否启用缓存机制 + */ + constructor(cache: boolean = true) { + super(cache); + this.renderFn = () => {}; + } + + protected render(canvas: MotaOffscreenCanvas2D): void { + canvas.ctx.save(); + this.renderFn(canvas); + canvas.ctx.restore(); + } + + setRenderFn(fn: CustomRenderFunction) { + 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; + case 'bindings': + if (!this.assertType(nextValue, Array, key)) return false; + if (nextValue !== prevValue) { + this.update(); + } else if (nextValue.length !== prevValue.length) { + this.update(); + } else { + const arr: Ref[] = nextValue as Ref[]; + const prev: Ref[] = prevValue as Ref[]; + for (let i = 0; i < nextValue.length; i++) { + if (arr[i].value !== prev[i].value) { + this.update(); + break; + } + } + } + return true; + } + return false; + } +} diff --git a/packages/render/src/core/event.ts b/packages/render/src/core/event.ts index d34b9d0..6a0999e 100644 --- a/packages/render/src/core/event.ts +++ b/packages/render/src/core/event.ts @@ -1,4 +1,4 @@ -import type { RenderItem } from './item'; +import { IRenderItem } from './types'; export const enum MouseType { /** 没有按键按下 */ @@ -51,7 +51,7 @@ export const enum EventProgress { export interface IActionEventBase { /** 当前事件是监听的哪个元素 */ - target: RenderItem; + target: IRenderItem; /** 是触摸操作还是鼠标操作 */ touch: boolean; /** diff --git a/packages/render/src/core/gl2.ts b/packages/render/src/core/gl2.ts index ff575ad..c35d4ba 100644 --- a/packages/render/src/core/gl2.ts +++ b/packages/render/src/core/gl2.ts @@ -1,15 +1,33 @@ -import EventEmitter from 'eventemitter3'; import { logger } from '@motajs/common'; import { MotaOffscreenCanvas2D } from './canvas2d'; -import { ERenderItemEvent, RenderItem, RenderItemPosition } from './item'; +import { RenderItem } from './item'; import { Transform } from './transform'; import { isWebGL2Supported } from './utils'; import { SizedCanvasImageSource } from '../types'; - -export interface IGL2ProgramPrefix { - readonly VERTEX: string; - readonly FRAGMENT: string; -} +import { + AttribSetFn, + AttribType, + DrawArraysInstancedParam, + DrawArraysParam, + DrawElementsInstancedParam, + DrawElementsParam, + DrawParamsMap, + IGL2Program, + IGL2ProgramPrefix, + IShaderAttrib, + IShaderAttribArray, + IShaderIndices, + IShaderTexture2D, + IShaderUniform, + IShaderUniformBlock, + IShaderUniformMatrix, + IWebGL2RenderItem, + ProgramConstructor, + RenderMode, + UniformMatrix, + UniformSetFn, + UniformType +} from './types'; const GL2_PREFIX: IGL2ProgramPrefix = { VERTEX: /* glsl */ `#version 300 es @@ -25,78 +43,7 @@ interface CompiledShader { fragment: WebGLShader; } -const enum RenderMode { - Arrays, - Elements, - ArraysInstanced, - ElementsInstanced -} - -export const enum UniformType { - Uniform1f, - Uniform1fv, - Uniform1i, - Uniform1iv, - Uniform1ui, - Uniform1uiv, - Uniform2f, - Uniform2fv, - Uniform2i, - Uniform2iv, - Uniform2ui, - Uniform2uiv, - Uniform3f, - Uniform3fv, - Uniform3i, - Uniform3iv, - Uniform3ui, - Uniform3uiv, - Uniform4f, - Uniform4fv, - Uniform4i, - Uniform4iv, - Uniform4ui, - Uniform4uiv -} - -export const enum UniformMatrix { - UMatrix2x2, - UMatrix2x3, - UMatrix2x4, - UMatrix3x2, - UMatrix3x3, - UMatrix3x4, - UMatrix4x2, - UMatrix4x3, - UMatrix4x4 -} - -export const enum AttribType { - Attrib1f, - Attrib1fv, - Attrib2f, - Attrib2fv, - Attrib3f, - Attrib3fv, - Attrib4f, - Attrib4fv, - AttribI4i, - AttribI4iv, - AttribI4ui, - AttribI4uiv -} - -export type ProgramConstructor = new ( - gl2: GL2, - vs?: string, - fs?: string -) => T; - -export interface EGL2Event extends ERenderItemEvent {} - -export abstract class GL2 extends RenderItem< - EGL2Event | E -> { +export abstract class GL2 extends RenderItem { /** 是否支持此组件 */ static readonly support: boolean = isWebGL2Supported(); @@ -161,14 +108,14 @@ export abstract class GL2 extends RenderItem< gl: WebGL2RenderingContext; /** webgl使用的程序 */ - protected program: GL2Program | null = null; + protected program: IGL2Program | null = null; /** 当前渲染实例的所有着色器程序 */ - protected programs: Set = new Set(); + protected programs: Set = new Set(); /** framebuffer 映射 */ protected framebufferMap: Map = new Map(); - constructor(type: RenderItemPosition = 'static') { - super(type, false); + constructor() { + super(false); this.canvas = document.createElement('canvas'); const gl = this.canvas.getContext('webgl2')!; @@ -258,7 +205,7 @@ export abstract class GL2 extends RenderItem< * @param gl 当前正在渲染的 gl2 画布 * @param program 当前元素正在使用的着色器程序 */ - draw(gl: WebGL2RenderingContext, program: GL2Program) { + protected draw(gl: WebGL2RenderingContext, program: IGL2Program) { const indices = program.usingIndices; const param = program.getDrawParams(program.renderMode); if (!param) return; @@ -367,7 +314,7 @@ export abstract class GL2 extends RenderItem< * 切换着色器程序 * @param program 着色器程序 */ - useProgram(program: GL2Program) { + useProgram(program: IGL2Program) { if (!this.gl) return; if (program.element !== this) { logger.error(17); @@ -386,11 +333,11 @@ export abstract class GL2 extends RenderItem< * @param vs 顶点着色器,可选 * @param fs 片元着色器,可选 */ - createProgram( + createProgram( Program: ProgramConstructor, vs?: string, fs?: string - ) { + ): T { const program = new Program(this, vs, fs); this.programs.add(program); return program; @@ -400,7 +347,7 @@ export abstract class GL2 extends RenderItem< * 删除一个着色器程序 * @param program 要删除的着色器程序 */ - deleteProgram(program: GL2Program) { + deleteProgram(program: IGL2Program) { if (program.element !== this) { logger.error(18); return; @@ -411,319 +358,18 @@ export abstract class GL2 extends RenderItem< destroy(): void { this.programs.forEach(v => v.destroy()); + this.programs.clear(); this.canvas.remove(); super.destroy(); } } -type _U1 = [x0: number]; -type _U2 = [x0: number, x1: number]; -type _U3 = [x0: number, x1: number, x2: number]; -type _U4 = [x0: number, x1: number, x2: number, x3: number]; -type _UV = [data: T, srcOffset?: number, srcLength?: number]; -type _A = [data: T]; - -interface UniformSetFn { - [UniformType.Uniform1f]: _U1; - [UniformType.Uniform1fv]: _UV; - [UniformType.Uniform1i]: _U1; - [UniformType.Uniform1iv]: _UV; - [UniformType.Uniform1ui]: _U1; - [UniformType.Uniform1uiv]: _UV; - [UniformType.Uniform2f]: _U2; - [UniformType.Uniform2fv]: _UV; - [UniformType.Uniform2i]: _U2; - [UniformType.Uniform2iv]: _UV; - [UniformType.Uniform2ui]: _U2; - [UniformType.Uniform2uiv]: _UV; - [UniformType.Uniform3f]: _U3; - [UniformType.Uniform3fv]: _UV; - [UniformType.Uniform3i]: _U3; - [UniformType.Uniform3iv]: _UV; - [UniformType.Uniform3ui]: _U3; - [UniformType.Uniform3uiv]: _UV; - [UniformType.Uniform4f]: _U4; - [UniformType.Uniform4fv]: _UV; - [UniformType.Uniform4i]: _U4; - [UniformType.Uniform4iv]: _UV; - [UniformType.Uniform4ui]: _U4; - [UniformType.Uniform4uiv]: _UV; -} - -interface AttribSetFn { - [AttribType.Attrib1f]: _U1; - [AttribType.Attrib1fv]: _A; - [AttribType.Attrib2f]: _U2; - [AttribType.Attrib2fv]: _A; - [AttribType.Attrib3f]: _U3; - [AttribType.Attrib3fv]: _A; - [AttribType.Attrib4f]: _U4; - [AttribType.Attrib4fv]: _A; - [AttribType.AttribI4i]: _U4; - [AttribType.AttribI4iv]: _A; - [AttribType.AttribI4ui]: _U4; - [AttribType.AttribI4uiv]: _A; -} - -export interface IShaderUniform { - /** 这个 uniform 变量的内存位置 */ - readonly location: WebGLUniformLocation; - /** 这个 uniform 变量的类型 */ - readonly type: T; - /** 这个量所处的着色器程序 */ - readonly program: GL2Program; - /** - * 设置这个 uniform 变量的值, - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/uniform - * @param params 要传递的参数,例如 uniform2f 就要传递 x0 x1 两个参数等,可以参考 mdn 文档 - */ - set(...params: UniformSetFn[T]): void; -} - -export interface IShaderAttrib { - /** 这个 attribute 常量的内存位置 */ - readonly location: number; - /** 这个 attribute 常量的类型 */ - readonly type: T; - /** 这个量所处的着色器程序 */ - readonly program: GL2Program; - /** - * 设置这个 attribute 常量的值, - * 浮点数参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/vertexAttrib - * 整数参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribI - * @param params 要传递的参数 - */ - set(...params: AttribSetFn[T]): void; -} - -export interface IShaderAttribArray { - /** 这个 attribute 常量的内存位置 */ - readonly location: number; - /** 这个 attribute 所用的缓冲区信息 */ - readonly data: WebGLBuffer; - /** 这个量所处的着色器程序 */ - readonly program: GL2Program; - /** - * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData - * @param data 数据 - * @param usage 用途 - */ - buffer(data: AllowSharedBufferSource | null, usage: GLenum): void; - /** - * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData - * @param data 数据 - * @param usage 用途 - * @param srcOffset 数据偏移量 - * @param length 数据长度 - */ - buffer( - data: ArrayBufferView, - usage: GLenum, - srcOffset: number, - length?: number - ): void; - /** - * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 - * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData - * @param dstByteOffset 数据修改的起始位置 - * @param srcData 数据 - */ - sub(dstByteOffset: GLintptr, srcData: AllowSharedBufferSource): void; - /** - * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 - * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData - * @param dstByteOffset 数据修改的起始位置 - * @param srcData 数据 - * @param srcOffset 数据偏移量 - * @param length 数据长度 - */ - sub( - dstByteOffset: GLintptr, - srcData: ArrayBufferView, - srcOffset: number, - length?: GLuint - ): void; - /** - * 告诉 gpu 将读取此 attribute 数据 - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/vertexAttribPointer - * @param size 单个数据大小 - * @param type 数据类型 - * @param normalized 是否要经过归一化处理 - * @param stride 每一部分字节偏移量 - * @param offset 第一部分字节偏移量 - */ - pointer( - size: GLint, - type: GLenum, - normalized: GLboolean, - stride: GLsizei, - offset: GLintptr - ): void; - /** - * 告诉 gpu 将由整数类型读取此 attribute 数据 - * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribIPointer - * @param size 单个数据大小 - * @param type 数据类型 - * @param stride 每一部分字节偏移量 - * @param offset 第一部分字节偏移量 - */ - pointerI( - size: GLint, - type: GLenum, - stride: GLsizei, - offset: GLintptr - ): void; - /** - * 设置顶点指针更新时刻。 - * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribDivisor - * @param divisor 每多少个实例更新一次,0表示每个顶点都更新 - */ - divisor(divisor: number): void; - /** - * 启用这个顶点数据 - */ - enable(): void; - /** - * 禁用这个顶点数据 - */ - disable(): void; -} - -export interface IShaderIndices { - /** 这个顶点索引所用的缓冲区信息 */ - readonly data: WebGLBuffer; - /** 这个量所处的着色器程序 */ - readonly program: GL2Program; - /** - * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData - * @param data 数据 - * @param usage 用途 - */ - buffer(data: AllowSharedBufferSource | null, usage: GLenum): void; - /** - * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData - * @param data 数据 - * @param usage 用途 - * @param srcOffset 数据偏移量 - * @param length 数据长度 - */ - buffer( - data: ArrayBufferView, - usage: GLenum, - srcOffset: number, - length?: number - ): void; - /** - * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 - * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData - * @param dstByteOffset 数据修改的起始位置 - * @param srcData 数据 - */ - sub(dstByteOffset: GLintptr, srcData: AllowSharedBufferSource): void; - /** - * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 - * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData - * @param dstByteOffset 数据修改的起始位置 - * @param srcData 数据 - * @param srcOffset 数据偏移量 - * @param length 数据长度 - */ - sub( - dstByteOffset: GLintptr, - srcData: ArrayBufferView, - srcOffset: number, - length?: GLuint - ): void; -} - -export interface IShaderUniformMatrix { - /** 矩阵的内存位置 */ - readonly location: WebGLUniformLocation; - /** 矩阵类型 */ - readonly type: UniformMatrix; - /** 这个量所处的着色器程序 */ - readonly program: GL2Program; - /** - * 设置矩阵的值,参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/uniformMatrix - * @param transpose 是否转置矩阵 - * @param data 矩阵数据,列主序 - * @param srcOffset 数据偏移量 - * @param srcLength 数据长度 - */ - set( - transpose: GLboolean, - data: Float32List, - srcOffset?: number, - srcLength?: number - ): void; -} - -export interface IShaderUniformBlock { - /** 这个 uniform block 的内存地址 */ - readonly location: GLuint; - /** 与这个 uniform block 所绑定的缓冲区 */ - readonly buffer: WebGLBuffer; - /** 这个 uniform block 的大小 */ - readonly size: number; - /** 这个量所处的着色器程序 */ - readonly program: GL2Program; - /** - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/bindBufferBase - * @param srcData 要设置为的值 - */ - set(srcData: AllowSharedBufferSource | null): void; - /** - * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/bindBufferBase - * @param srcData 要设置为的值 - * @param srcOffset 数据偏移量 - * @param length 数据长度 - */ - set(srcData: ArrayBufferView, srcOffset: number, length?: number): void; -} - -export interface IShaderTexture2D { - /** 纹理对象 */ - readonly texture: WebGLTexture; - /** 宽度 */ - readonly width: number; - /** 高度 */ - readonly height: number; - /** 纹理所属索引 */ - readonly index: number; - /** 这个量所处的着色器程序 */ - readonly program: GL2Program; - /** - * 设置这个纹理的图像,不建议使用,会修改宽高 - * @param source 要设置成的图像源 - */ - set(source: TexImageSource): void; - /** - * 设置纹理的一部分信息,不会修改宽高 - * @param source 要设置的图像源 - * @param x 要设置到的起始点横坐标 - * @param y 要设置到的起始点纵坐标 - * @param width 宽度 - * @param height 高度 - */ - sub( - source: TexImageSource, - x: number, - y: number, - width: number, - height: number - ): void; -} - class ShaderUniform implements IShaderUniform { constructor( readonly type: T, readonly location: WebGLUniformLocation, readonly gl: WebGL2RenderingContext, - readonly program: GL2Program + readonly program: IGL2Program ) {} set(...params: UniformSetFn[T]): void { @@ -812,7 +458,7 @@ class ShaderAttrib implements IShaderAttrib { readonly type: T, readonly location: number, readonly gl: WebGL2RenderingContext, - readonly program: GL2Program + readonly program: IGL2Program ) {} set(...params: AttribSetFn[T]) { @@ -869,7 +515,7 @@ class ShaderAttribArray implements IShaderAttribArray { readonly data: WebGLBuffer, readonly location: number, readonly gl: WebGL2RenderingContext, - readonly program: GL2Program + readonly program: IGL2Program ) {} buffer(data: AllowSharedBufferSource | null, usage: GLenum): void; @@ -954,7 +600,7 @@ class ShaderIndices implements IShaderIndices { constructor( readonly data: WebGLBuffer, readonly gl: WebGL2RenderingContext, - readonly program: GL2Program + readonly program: IGL2Program ) {} buffer(data: AllowSharedBufferSource | null, usage: GLenum): void; @@ -999,7 +645,7 @@ class ShaderUniformMatrix implements IShaderUniformMatrix { readonly type: UniformMatrix, readonly location: WebGLUniformLocation, readonly gl: WebGL2RenderingContext, - readonly program: GL2Program + readonly program: IGL2Program ) {} set(x2: GLboolean, x3: Float32List, x4?: number, x5?: number): void { @@ -1043,7 +689,7 @@ class ShaderUniformBlock implements IShaderUniformBlock { readonly buffer: WebGLBuffer, readonly binding: number, readonly gl: WebGL2RenderingContext, - readonly program: GL2Program + readonly program: IGL2Program ) {} set(srcData: AllowSharedBufferSource | null): void; @@ -1070,7 +716,7 @@ class ShaderTexture2D implements IShaderTexture2D { readonly index: number, readonly uniform: IShaderUniform, readonly gl: WebGL2RenderingContext, - readonly program: GL2Program, + readonly program: IGL2Program, public width: number = 0, public height: number = 0 ) { @@ -1136,47 +782,7 @@ class ShaderTexture2D implements IShaderTexture2D { } } -interface DrawArraysParam { - mode: GLenum; - first: number; - count: number; -} - -interface DrawElementsParam { - mode: GLenum; - count: number; - type: GLenum; - offset: GLintptr; -} - -interface DrawArraysInstancedParam { - mode: GLenum; - first: number; - count: number; - instanceCount: number; -} - -interface DrawElementsInstancedParam { - mode: GLenum; - count: number; - type: GLenum; - offset: GLintptr; - instanceCount: number; -} - -export interface DrawParamsMap { - [RenderMode.Arrays]: DrawArraysParam; - [RenderMode.ArraysInstanced]: DrawArraysInstancedParam; - [RenderMode.Elements]: DrawElementsParam; - [RenderMode.ElementsInstanced]: DrawElementsInstancedParam; -} - -interface ShaderProgramEvent { - load: []; - unload: []; -} - -export class GL2Program extends EventEmitter { +export class GL2Program implements IGL2Program { /** 顶点着色器 */ private vertex: string = ''; /** 片元着色器 */ @@ -1184,7 +790,7 @@ export class GL2Program extends EventEmitter { /** webgl2上下文 */ gl: WebGL2RenderingContext; /** 当前着色器程序的着色器渲染元素 */ - element: GL2; + element: IWebGL2RenderItem; /** uniform存放地址 */ private uniform: Map> = new Map(); @@ -1220,8 +826,7 @@ export class GL2Program extends EventEmitter { /** 着色器代码的前缀,会在设置时自动添加至代码前 */ protected readonly prefix: IGL2ProgramPrefix = GL2_PREFIX; - constructor(shader: GL2, vs?: string, fs?: string) { - super(); + constructor(shader: IWebGL2RenderItem, vs?: string, fs?: string) { if (vs) this.vs(vs); if (fs) this.fs(fs); this.element = shader; @@ -1377,6 +982,7 @@ export class GL2Program extends EventEmitter { /** * 检查当前是否需要重新编译着色器,如果需要,则重新编译 * @param force 是否强制重新编译 + * @returns 是否执行了编译操作 */ requestCompile(force: boolean = false): boolean { if (!force && !this.shaderDirty) return false; @@ -1408,7 +1014,6 @@ export class GL2Program extends EventEmitter { this.attribArray.forEach(v => { v.disable(); }); - this.emit('load'); } /** @@ -1418,7 +1023,6 @@ export class GL2Program extends EventEmitter { this.attribArray.forEach(v => { v.enable(); }); - this.emit('unload'); } /** diff --git a/packages/render/src/elements/graphics.ts b/packages/render/src/core/graphics.ts similarity index 91% rename from packages/render/src/elements/graphics.ts rename to packages/render/src/core/graphics.ts index 3543dc5..eebc2f6 100644 --- a/packages/render/src/elements/graphics.ts +++ b/packages/render/src/core/graphics.ts @@ -1,100 +1,23 @@ -import { - Transform, - ERenderItemEvent, - RenderItem, - MotaOffscreenCanvas2D -} from '../core'; +import { Transform, RenderItem, MotaOffscreenCanvas2D } from '.'; import { CanvasStyle } from '../types'; import { logger } from '@motajs/common'; import { clamp, isEqual, isNil } from 'lodash-es'; - -export type CircleParams = [ - cx?: number, - cy?: number, - radius?: number, - start?: number, - end?: number -]; -export type EllipseParams = [ - cx?: number, - cy?: number, - radiusX?: number, - radiusY?: number, - start?: number, - end?: number -]; -export type LineParams = [x1: number, y1: number, x2: number, y2: number]; -export type BezierParams = [ - sx: number, - sy: number, - cp1x: number, - cp1y: number, - cp2x: number, - cp2y: number, - ex: number, - ey: number -]; -export type QuadParams = [ - sx: number, - sy: number, - cpx: number, - cpy: number, - ex: number, - ey: number -]; -export type RectRCircleParams = [ - r1: number, - r2?: number, - r3?: number, - r4?: number -]; -export type RectREllipseParams = [ - rx1: number, - ry1: number, - rx2?: number, - ry2?: number, - rx3?: number, - ry3?: number, - rx4?: number, - ry4?: number -]; - -export interface ILineProperty { - /** 线宽 */ - lineWidth: number; - /** 线的虚线设置 */ - lineDash?: number[]; - /** 虚线偏移量 */ - lineDashOffset?: number; - /** 线的连接样式 */ - lineJoin: CanvasLineJoin; - /** 线的顶端样式 */ - lineCap: CanvasLineCap; - /** 线的斜接限制,当连接为miter类型时可填,默认为10 */ - miterLimit: number; -} - -export interface IGraphicProperty extends ILineProperty { - /** 渲染模式,参考 {@link GraphicMode} */ - mode: GraphicMode; - /** 填充样式 */ - fill: CanvasStyle; - /** 描边样式 */ - stroke: CanvasStyle; - /** 填充算法 */ - fillRule: CanvasFillRule; -} - -export const enum GraphicMode { - /** 仅填充 */ - Fill, - /** 仅描边 */ - Stroke, - /** 先填充,然后描边 */ - FillAndStroke, - /** 先描边,然后填充 */ - StrokeAndFill -} +import { + ILineProperty, + GraphicMode, + IGraphicRenderItem, + IGraphicCircle, + IGraphicEllipse, + IGraphicLine, + IGraphicBezierCurve, + IGraphicQuadBezierCurve, + IGraphicPath, + RectRCorner, + CircleParams, + EllipseParams, + RectRCircleParams, + RectREllipseParams +} from './types'; const enum GraphicModeProp { Fill, @@ -102,11 +25,13 @@ const enum GraphicModeProp { StrokeAndFill } -export interface EGraphicItemEvent extends ERenderItemEvent {} +/** 用于点击检测的画布 */ +const testCanvas = new MotaOffscreenCanvas2D(false); +testCanvas.size(1, 1); export abstract class GraphicItemBase - extends RenderItem - implements Required + extends RenderItem + implements Required, IGraphicRenderItem { mode: GraphicMode = GraphicMode.Fill; fill: CanvasStyle = '#ddd'; @@ -118,7 +43,6 @@ export abstract class GraphicItemBase lineCap: CanvasLineCap = 'butt'; miterLimit: number = 10; fillRule: CanvasFillRule = 'nonzero'; - enableCache: boolean = false; private propFill: boolean = true; private propStroke: boolean = false; @@ -129,6 +53,10 @@ export abstract class GraphicItemBase private cachePath?: Path2D; protected pathDirty: boolean = true; + constructor(enableCache: boolean = false) { + super(enableCache); + } + /** * 获取这个元素的绘制路径 */ @@ -166,7 +94,7 @@ export abstract class GraphicItemBase } protected isActionInElement(x: number, y: number): boolean { - const ctx = this.cache.ctx; + const ctx = testCanvas.ctx; if (this.pathDirty) { this.cachePath = this.getPath(); this.pathDirty = false; @@ -387,7 +315,7 @@ export class Rect extends GraphicItemBase { } } -export class Circle extends GraphicItemBase { +export class Circle extends GraphicItemBase implements IGraphicCircle { radius: number = 10; start: number = 0; end: number = Math.PI * 2; @@ -462,7 +390,7 @@ export class Circle extends GraphicItemBase { } } -export class Ellipse extends GraphicItemBase { +export class Ellipse extends GraphicItemBase implements IGraphicEllipse { radiusX: number = 10; radiusY: number = 10; start: number = 0; @@ -552,7 +480,7 @@ export class Ellipse extends GraphicItemBase { } } -export class Line extends GraphicItemBase { +export class Line extends GraphicItemBase implements IGraphicLine { x1: number = 0; y1: number = 0; x2: number = 0; @@ -633,7 +561,10 @@ export class Line extends GraphicItemBase { } } -export class BezierCurve extends GraphicItemBase { +export class BezierCurve + extends GraphicItemBase + implements IGraphicBezierCurve +{ sx: number = 0; sy: number = 0; cp1x: number = 0; @@ -700,10 +631,6 @@ export class BezierCurve extends GraphicItemBase { this.update(); } - protected isActionInElement(x: number, y: number): boolean { - return x >= 0 && x < this.width && y >= 0 && y < this.height; - } - private fitRect() { const left = Math.min(this.sx, this.cp1x, this.cp2x, this.ex); const top = Math.min(this.sy, this.cp1y, this.cp2y, this.ey); @@ -767,7 +694,10 @@ export class BezierCurve extends GraphicItemBase { } } -export class QuadraticCurve extends GraphicItemBase { +export class QuadraticCurve + extends GraphicItemBase + implements IGraphicQuadBezierCurve +{ sx: number = 0; sy: number = 0; cpx: number = 0; @@ -842,10 +772,6 @@ export class QuadraticCurve extends GraphicItemBase { this.pathDirty = true; } - protected isActionInElement(x: number, y: number): boolean { - return x >= 0 && x < this.width && y >= 0 && y < this.height; - } - protected handleProps( key: string, prevValue: any, @@ -890,7 +816,7 @@ export class QuadraticCurve extends GraphicItemBase { } } -export class Path extends GraphicItemBase { +export class Path extends GraphicItemBase implements IGraphicPath { /** 路径 */ path: Path2D = new Path2D(); @@ -901,6 +827,14 @@ export class Path extends GraphicItemBase { return this.path; } + /** + * 重置此元素的路径 + */ + resetPath() { + this.path = new Path2D(); + this.update(); + } + /** * 为路径添加路径 * @param path 要添加的路径 @@ -911,10 +845,6 @@ export class Path extends GraphicItemBase { this.update(); } - protected isActionInElement(x: number, y: number): boolean { - return x >= 0 && x < this.width && y >= 0 && y < this.height; - } - protected handleProps( key: string, prevValue: any, @@ -932,13 +862,6 @@ export class Path extends GraphicItemBase { } } -export const enum RectRCorner { - TopLeft, - TopRight, - BottomRight, - BottomLeft -} - export class RectR extends GraphicItemBase { /** 圆角属性,四元素数组,每个元素是一个二元素数组,表示这个角的半径,顺序为 左上,右上,右下,左下 */ readonly corner: [radiusX: number, radiusY: number][] = [ diff --git a/packages/render/src/core/index.ts b/packages/render/src/core/index.ts index 02de53c..2ba497c 100644 --- a/packages/render/src/core/index.ts +++ b/packages/render/src/core/index.ts @@ -1,11 +1,14 @@ export * from './adapter'; export * from './canvas2d'; export * from './container'; +export * from './custom'; export * from './event'; export * from './gl2'; +export * from './graphics'; export * from './item'; +export * from './misc'; export * from './render'; export * from './shader'; -export * from './sprite'; export * from './transform'; +export * from './types'; export * from './utils'; diff --git a/packages/render/src/core/item.ts b/packages/render/src/core/item.ts index 79d041e..1acd919 100644 --- a/packages/render/src/core/item.ts +++ b/packages/render/src/core/item.ts @@ -1,11 +1,9 @@ import { isEqual, isNil } from 'lodash-es'; import { EventEmitter } from 'eventemitter3'; import { MotaOffscreenCanvas2D } from './canvas2d'; -import { Ticker, TickerFn } from 'mutate-animate'; import { ITransformUpdatable, Transform } from './transform'; import { logger } from '@motajs/common'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; -import { transformCanvas } from './utils'; import { ActionEventMap, ActionType, @@ -16,261 +14,56 @@ import { MouseType } from './event'; import { vec3 } from 'gl-matrix'; - -export type RenderFunction = ( - canvas: MotaOffscreenCanvas2D, - transform: Transform -) => void; - -export type RenderItemPosition = 'absolute' | 'static'; - -export interface IRenderUpdater { - /** - * 更新这个渲染元素 - * @param item 触发更新事件的元素,不填默认为元素自身触发 - */ - update(item?: RenderItem): void; -} - -export interface IRenderAnchor { - /** 锚点横坐标,0表示最左端,1表示最右端 */ - anchorX: number; - /** 锚点纵坐标,0表示最上端,1表示最下端 */ - anchorY: number; - - /** - * 设置渲染元素的位置锚点 - * @param x 锚点的横坐标,小数,0表示最左边,1表示最右边 - * @param y 锚点的纵坐标,小数,0表示最上边,1表示最下边 - */ - setAnchor(x: number, y: number): void; -} - -export interface IRenderConfig { - /** 是否是高清画布 */ - highResolution: boolean; - /** 是否启用抗锯齿 */ - antiAliasing: boolean; - - /** - * 设置当前渲染元素是否使用高清画布 - * @param hd 是否高清 - */ - setHD(hd: boolean): void; - - /** - * 设置当前渲染元素是否启用抗锯齿 - * @param anti 是否抗锯齿 - */ - setAntiAliasing(anti: boolean): void; -} - -export interface IRenderChildable { - /** 当前元素的子元素 */ - children: Set; - - /** - * 向这个元素添加子元素 - * @param child 添加的元素 - */ - appendChild(...child: RenderItem[]): void; - - /** - * 移除这个元素中的某个子元素 - * @param child 要移除的元素 - */ - removeChild(...child: RenderItem[]): void; - - /** - * 在下一个tick的渲染前对子元素进行排序 - */ - requestSort(): void; -} - -export interface IRenderFrame { - /** - * 在下一帧渲染之前执行函数,常用于渲染前数据更新,理论上不应当用于渲染,不保证运行顺序 - * @param fn 执行的函数 - */ - requestBeforeFrame(fn: () => void): void; - - /** - * 在下一帧渲染之后执行函数,理论上不应当用于渲染,不保证运行顺序 - * @param fn 执行的函数 - */ - requestAfterFrame(fn: () => void): void; - - /** - * 在下一帧渲染时执行函数,理论上应当只用于渲染(即{@link RenderItem.update}方法),且不保证运行顺序 - * @param fn 执行的函数 - */ - requestRenderFrame(fn: () => void): void; -} - -export interface IRenderTickerSupport { - /** - * 委托ticker,让其在指定时间范围内每帧执行对应函数,超过时间后自动删除 - * @param fn 每帧执行的函数 - * @param time 函数持续时间,不填代表不会自动删除,需要手动删除 - * @param end 持续时间结束后执行的函数 - * @returns 委托id,可用于删除 - */ - delegateTicker(fn: TickerFn, time?: number, end?: () => void): number; - - /** - * 移除ticker函数 - * @param id 函数id,也就是{@link IRenderTickerSupport.delegateTicker}的返回值 - * @param callEnd 是否调用结束函数,即{@link IRenderTickerSupport.delegateTicker}的end参数,默认调用 - * @returns 是否删除成功,比如对应ticker不存在,就是删除失败 - */ - removeTicker(id: number, callEnd?: boolean): boolean; - - /** - * 检查是否包含一个委托函数 - * @param id 函数id - */ - hasTicker(id: number): boolean; -} - -export interface IRenderEvent { - /** - * 当触发缩放事件时,此函数执行的内容 - * @param scale 缩放至的缩放比 - */ - onResize(scale: number): void; -} - -export interface IRenderVueSupport { - /** - * 在 jsx, vue 中当属性改变后触发此函数,用于处理响应式等情况 - * @param key 属性键名 - * @param prevValue 该属性先前的数值 - * @param nextValue 该属性当前的数值 - * @param namespace 元素命名空间 - * @param parentComponent 元素的父组件 - */ - patchProp( - key: string, - prevValue: any, - nextValue: any, - namespace?: ElementNamespace, - parentComponent?: ComponentInternalInstance | null - ): void; -} - -export interface IRenderTreeRoot { - readonly isRoot: true; - - /** - * 将一个渲染元素连接到此根元素 - * @param item 要连接到此根元素的渲染元素 - */ - connect(item: RenderItem): void; - - /** - * 将已连接的渲染元素从此根元素中去掉 - * @param item 要取消连接的渲染元素 - */ - disconnect(item: RenderItem): void; - - /** - * 修改已连接的元素的 id - * @param item 修改了 id 的元素 - * @param previous 先前的元素 id - * @param current 现在的元素 id - */ - modifyId(item: RenderItem, previous: string, current: string): void; - - /** - * 获取渲染至的目标画布,即显示在画面上的画布 - */ - getCanvas(): HTMLCanvasElement; - - /** - * 当鼠标覆盖在某个元素上时执行 - * @param element 鼠标覆盖的元素 - */ - hoverElement(element: RenderItem): void; -} +import { + ERenderItemEvent, + ExcitableDelegation, + IRenderItem, + IRenderTreeRoot, + RenderPosition +} from './types'; +import { transformCanvas } from './utils'; interface RenderItemCanvasData { autoScale: boolean; } -export interface ERenderItemEvent extends ERenderItemActionEvent { - beforeRender: [transform: Transform]; - afterRender: [transform: Transform]; - destroy: []; - transform: [item: RenderItem, transform: Transform]; +interface ToDelegateExcitables { + readonly excitable: ExcitableDelegation; + readonly time?: number; + readonly end?: () => void; } -interface TickerDelegation { - fn: TickerFn; - timeout?: number; - endFn?: () => void; -} - -const beforeFrame: (() => void)[] = []; -const afterFrame: (() => void)[] = []; -const renderFrame: (() => void)[] = []; - -let count = 0; -export abstract class RenderItem - extends EventEmitter - implements - IRenderUpdater, - IRenderAnchor, - IRenderConfig, - IRenderFrame, - IRenderTickerSupport, - IRenderChildable, - IRenderVueSupport, - ITransformUpdatable, - IRenderEvent +export abstract class RenderItem + extends EventEmitter + implements IRenderItem, ITransformUpdatable { - /** 渲染的全局ticker */ - static ticker: Ticker = new Ticker(); - /** 包括但不限于怪物、npc、自动元件的动画帧数 */ - static animatedFrame: number = 0; - /** ticker委托映射 */ - static tickerMap: Map = new Map(); - /** ticker委托id */ - static tickerId: number = 0; - - readonly uid: number = count++; - //#region 元素属性 + /** 元素唯一标识符 */ + uid: number = 0; /** 是否是注释元素 */ readonly isComment: boolean = false; - - private _id: string = ''; - /** - * 元素的 id,原则上不可重复 - */ - get id(): string { - return this._id; - } - set id(v: string) { - this.checkRoot(); - const prev = this._id; - this._id = v; - this._root?.modifyId(this, prev, v); - } + /** 元素标识符 */ + id: string = ''; /** 元素纵深,表示了遮挡关系 */ zIndex: number = 0; - + /** 元素横坐标 */ + x: number = 0; + /** 元素纵坐标 */ + y: number = 0; + /** 元素宽度 */ width: number = 200; + /** 元素高度 */ height: number = 200; - + /** 该元素的变换矩阵 */ + transform: Transform = new Transform(); /** 渲染锚点,(0,0)表示左上角,(1,1)表示右下角 */ anchorX: number = 0; /** 渲染锚点,(0,0)表示左上角,(1,1)表示右下角 */ anchorY: number = 0; - - /** 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动 */ - type: RenderItemPosition = 'static'; + /** 渲染定位模式 */ + position: RenderPosition = RenderPosition.Absolute; /** 是否是高清画布 */ highResolution: boolean = true; /** 是否抗锯齿 */ @@ -284,67 +77,38 @@ export abstract class RenderItem /** 不透明度 */ alpha: number = 1; /** 缩放比 */ - protected scale: number = 1; - + scale: number = 1; /** 鼠标覆盖在此元素上时的光标样式 */ cursor: string = 'inherit'; + /** 该元素是否忽略交互事件 */ noEvent: boolean = false; - get x() { - return this._transform.x; - } - get y() { - return this._transform.y; - } - - /** 该元素的变换矩阵 */ - private _transform: Transform = new Transform(); - set transform(value: Transform) { - this._transform.unbind(this); - this._transform = value; - value.bind(this); - } - get transform() { - return this._transform; - } - //#endregion //#region 父子关系 - private _parent?: RenderItem; - /** 当前元素的父元素 */ - get parent() { - return this._parent; - } - /** 当前元素是否为根元素,如果是根元素,那么必须实现 `IRenderTreeRoot` 接口 */ + /** 当前元素是否为根元素 */ readonly isRoot: boolean = false; - - private _root?: RenderItem & IRenderTreeRoot; - get root() { - return this._root; - } - - /** 当前元素是否已经连接至任意根元素 */ - get connected() { - return !!this._root; - } - + /** 父元素对象 */ + parent: RenderItem | null = null; + /** 此元素的根元素 */ + root: IRenderTreeRoot | null = null; + /** 当前元素是否连接至了根元素 */ + connected: boolean = false; /** 该渲染元素的子元素 */ - children: Set> = new Set(); + children: Set = new Set(); //#endregion //#region 渲染配置与缓存 + /** 渲染缓存信息 */ - protected cache: MotaOffscreenCanvas2D; + protected readonly cache: MotaOffscreenCanvas2D | null; /** 是否需要更新缓存 */ protected cacheDirty: boolean = false; /** 是否启用缓存机制 */ readonly enableCache: boolean = true; - /** 是否启用transform下穿机制,即画布的变换是否会继续作用到下一层画布 */ - readonly transformFallThrough: boolean = false; /** 这个渲染元素使用到的所有画布 */ protected readonly canvases: Set = new Set(); /** 这个渲染元素每个画布的配置信息 */ @@ -352,6 +116,7 @@ export abstract class RenderItem MotaOffscreenCanvas2D, RenderItemCanvasData > = new WeakMap(); + //#endregion //#region 交互事件 @@ -360,14 +125,12 @@ export abstract class RenderItem protected propagationStoped: Map = new Map(); /** 捕获阶段缓存的事件对象 */ private cachedEvent: Map = new Map(); - /** 下穿模式下当前下穿过来的变换矩阵 */ - private fallTransform?: Transform; /** 是否在元素内 */ private inElement: boolean = false; /** 鼠标标识符映射,键为按下的鼠标按键类型,值表示本次操作的唯一标识符,在按下、移动、抬起过程中保持一致 */ - protected mouseId: Map = new Map(); + protected readonly mouseId: Map = new Map(); /** 当前所有的触摸标识符 */ - readonly touchId: Set = new Set(); + protected readonly touchId: Set = new Set(); //#endregion @@ -378,32 +141,48 @@ export abstract class RenderItem //#endregion - constructor( - type: RenderItemPosition, - enableCache: boolean = true, - transformFallThrough: boolean = false - ) { + //#region 其他 + + /** 存储当前渲染对象的所有委托激励对象,用于在被销毁时删除 */ + private delegationsIdMap: Map = new Map(); + /** 委托激励对象到其 id 的映射 */ + private delegationsMap: Map = new Map(); + + /** 当未绑定根元素时临时存储帧前函数 */ + private beforeFrames: Set<() => void> = new Set(); + /** 当未绑定根元素时临时存储帧后函数 */ + private afterFrames: Set<() => void> = new Set(); + /** 当未绑定根元素时临时存储委托可激励对象 */ + private toDelegate: Set = new Set(); + + //#endregion + + constructor(enableCache: boolean = true) { super(); - this.enableCache = enableCache; - this.transformFallThrough = transformFallThrough; - this.type = type; - - this._transform.bind(this); - this.cache = this.requireCanvas(); - if (!enableCache) { - this.cache.size(1, 1); - this.deleteCanvas(this.cache); + this.position = RenderPosition.Absolute; + this.transform.bind(this); + if (enableCache) { + this.cache = this.requireCanvas(); + } else { + this.cache = null; } } + setID(id: string): void { + this.checkRoot(); + const prev = this.id; + this.id = id; + this.root?.modifyId(this, prev, id); + } + + //#region 渲染部分 + /** * 渲染函数 * @param canvas 渲染至的画布 * @param transform 当前变换矩阵的,渲染时已经进行变换处理,不需要对画布再次进行变换处理。 * 此参数可用于自己对元素进行变换处理,也会用于对子元素的处理。 - * 例如对于`absolute`类型的元素,同时有对视角改变的需求,就可以通过此参数进行变换。 - * 样板内置的`Layer`及`Damage`元素就是通过此方式实现的 */ protected abstract render( canvas: MotaOffscreenCanvas2D, @@ -413,47 +192,75 @@ export abstract class RenderItem /** * 渲染当前对象 * @param canvas 渲染至的画布 - * @param transform 由父元素传递过来的变换矩阵 */ - renderContent(canvas: MotaOffscreenCanvas2D, transform: Transform) { + renderContent(canvas: MotaOffscreenCanvas2D) { if (this.hidden) return; this.forbidUpdate = true; - this.emit('beforeRender', transform); - if (this.transformFallThrough) { - this.fallTransform = transform; - } - const tran = this.transformFallThrough ? transform : this._transform; + const tran = this.transform; const ax = -this.anchorX * this.width; const ay = -this.anchorY * this.height; const ctx = canvas.ctx; ctx.save(); - if (this.type === 'static') transformCanvas(canvas, tran); + transformCanvas(canvas, tran); ctx.filter = this.filter; ctx.globalAlpha = this.alpha; ctx.globalCompositeOperation = this.composite; - if (this.enableCache) { - const { width, height } = this.cache; + if (this.enableCache && this.cache) { + const { width, height } = this.cache!; if (this.cacheDirty) { this.cache.clear(); this.render(this.cache, tran); this.cacheDirty = false; } - - canvas.setAntiAliasing(false); - canvas.ctx.drawImage(this.cache.canvas, ax, ay, width, height); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(this.cache.canvas, ax, ay, width, height); } else { - canvas.setAntiAliasing(this.antiAliasing); - canvas.ctx.translate(ax, ay); + ctx.imageSmoothingEnabled = this.antiAliasing; + ctx.translate(ax, ay); this.render(canvas, tran); this.cacheDirty = false; } ctx.restore(); - this.emit('afterRender', transform); this.forbidUpdate = false; } + update(item: IRenderItem = this): void { + if (import.meta.env.DEV) { + if (this.forbidUpdate) { + logger.warn(61, this.constructor.name, this.uid.toString()); + } + } + if (this.parent) { + if (this.cacheDirty && this.parent.cacheDirty) return; + this.cacheDirty = true; + if (this.hidden) return; + this.parent.update(item); + } else { + if (this.cacheDirty) return; + this.cacheDirty = true; + } + } + + /** + * 刷新所有子元素 + */ + refreshAllChildren() { + if (this.children.size > 0) { + const stack: RenderItem[] = [this]; + while (stack.length > 0) { + const item = stack.pop(); + if (!item) continue; + item.cacheDirty = true; + item.children.forEach(v => stack.push(v)); + } + } + this.update(); + } + + //#endregion + /** * 申请一个 `MotaOffscreenCanvas2D`,即申请一个画布 * @param alpha 是否启用画布的 alpha 通道 @@ -492,14 +299,9 @@ export abstract class RenderItem this.update(); } - /** - * 获取当前元素的缩放比,它与根元素应当保持一致 - */ - getScale() { - return this.scale; - } + //#endregion - //#region 修改元素属性 + //#region 渲染设置 /** * 修改这个对象的大小 @@ -509,9 +311,10 @@ export abstract class RenderItem this.width = width; this.height = height; if (this.enableCache) { - this.cache.size(width, height); + this.cache!.size(width, height); } - this.update(this); + this.update(); + this.emit('resize', width, height); } /** @@ -521,7 +324,19 @@ export abstract class RenderItem */ pos(x: number, y: number) { // 这个函数会调用 update,因此不再手动调用 update - this._transform.setTranslate(x, y); + this.transform.setTranslate(x, y); + } + + /** + * 设置元素变换矩阵 + * @param transform 变换矩阵对象 + */ + setTransform(transform: Transform): void { + this.transform.unbind(this); + this.transform = transform; + this.transform.bind(this); + this.x = transform.x; + this.y = transform.y; } /** @@ -531,7 +346,7 @@ export abstract class RenderItem setFilter(filter: string) { this.filter = filter; // 设置滤镜时,不需要更新自身的缓存,直接调用父元素的更新即可 - this._parent?.update(); + this.parent?.update(); } /** @@ -541,7 +356,7 @@ export abstract class RenderItem setComposite(composite: GlobalCompositeOperation) { this.composite = composite; // 设置混合模式时,不需要更新自身的缓存,直接调用父元素的更新即可 - this._parent?.update(); + this.parent?.update(); } /** @@ -551,23 +366,23 @@ export abstract class RenderItem setAlpha(alpha: number) { this.alpha = alpha; // 设置不透明度时,不需要更新自身的缓存,直接调用父元素的更新即可 - this._parent?.update(); + this.parent?.update(); } setHD(hd: boolean): void { this.highResolution = hd; if (this.enableCache) { - this.cache.setHD(hd); + this.cache!.setHD(hd); } - this.update(this); + this.update(); } setAntiAliasing(anti: boolean): void { this.antiAliasing = anti; if (this.enableCache) { - this.cache.setAntiAliasing(anti); + this.cache!.setAntiAliasing(anti); } - this.update(this); + this.update(); } setZIndex(zIndex: number) { @@ -579,7 +394,15 @@ export abstract class RenderItem this.anchorX = x; this.anchorY = y; // 设置锚点时,不需要更新自身的缓存,直接调用父元素的更新即可 - this._parent?.update(); + this.parent?.update(); + } + + /** + * 设置光标样式 + * @param cursor 光标样式 + */ + setCursor(cursor: string): void { + this.cursor = cursor; } /** @@ -588,7 +411,7 @@ export abstract class RenderItem hide() { if (this.hidden) return; this.hidden = true; - this.update(this); + this.update(); } /** @@ -608,11 +431,7 @@ export abstract class RenderItem * 获取当前元素的绝对位置(不建议使用,因为应当很少会有获取绝对位置的需求) */ getAbsolutePosition(x: number = 0, y: number = 0): [number, number] { - if (this.type === 'absolute') { - if (this.parent) return this.parent.getAbsolutePosition(0, 0); - else return [0, 0]; - } - const [px, py] = this._transform.transformed(x, y); + const [px, py] = this.transform.transformed(x, y); if (!this.parent) return [px, py]; else { const [px, py] = this.parent.getAbsolutePosition(); @@ -624,12 +443,7 @@ export abstract class RenderItem * 获取到可以包围这个元素的最小矩形,相对于父元素 */ getBoundingRect(): DOMRectReadOnly { - if (this.type === 'absolute') { - return new DOMRectReadOnly(0, 0, this.width, this.height); - } - const tran = this.transformFallThrough - ? this.fallTransform - : this._transform; + const tran = this.transform; if (!tran) return new DOMRectReadOnly(0, 0, this.width, this.height); const [x1, y1] = tran.transformed( -this.anchorX * this.width, @@ -654,137 +468,135 @@ export abstract class RenderItem return new DOMRectReadOnly(left, top, right - left, bottom - top); } - update(item: RenderItem = this): void { - if (import.meta.env.DEV) { - if (this.forbidUpdate) { - logger.warn(61, this.constructor.name, this.uid.toString()); - } - } - if (this._parent) { - if (this.cacheDirty && this._parent.cacheDirty) return; - this.cacheDirty = true; - if (this.hidden) return; - this._parent.update(item); - } else { - if (this.cacheDirty) return; - this.cacheDirty = true; - } - } - updateTransform(transform: Transform) { // 更新变换矩阵时,不需要更新自身的缓存,直接调用父元素的更新即可 - if (transform === this.transform) { - this._parent?.update(); - this.emit('transform', this, this._transform); - } + this.parent?.update(); + this.x = transform.x; + this.y = transform.y; + this.emit('transform', this, transform); } - //#endregion - - //#region 动画帧与 ticker - requestBeforeFrame(fn: () => void): void { - beforeFrame.push(fn); + if (!this.root) { + this.beforeFrames.add(fn); + } else { + this.root?.requestBeforeFrame(fn); + } } requestAfterFrame(fn: () => void): void { - afterFrame.push(fn); - } - - requestRenderFrame(fn: () => void): void { - renderFrame.push(fn); - } - - delegateTicker(fn: TickerFn, time?: number, end?: () => void): number { - const id = RenderItem.tickerId++; - if (typeof time === 'number' && time === 0) return id; - const delegation: TickerDelegation = { - fn, - endFn: end - }; - RenderItem.tickerMap.set(id, delegation); - if (typeof time === 'number' && time < 2147438647 && time > 0) { - delegation.timeout = window.setTimeout(() => { - RenderItem.tickerMap.delete(id); - end?.(); - }, time); + if (!this.root) { + this.afterFrames.add(fn); + } else { + this.root?.requestAfterFrame(fn); } - return id; } - removeTicker(id: number, callEnd: boolean = true): boolean { - const delegation = RenderItem.tickerMap.get(id); - if (!delegation) return false; - RenderItem.ticker.remove(delegation.fn); - window.clearTimeout(delegation.timeout); - if (callEnd) delegation.endFn?.(); - RenderItem.tickerMap.delete(id); - return true; + delegateExcitable( + excitable: ExcitableDelegation, + time?: number, + end?: () => void + ): number { + if (!this.root) { + this.toDelegate.add({ excitable, time, end }); + return -1; + } else { + const index = this.root.delegateExcitable(excitable, time, end); + this.delegationsIdMap.set(index, excitable); + this.delegationsMap.set(excitable, index); + return index; + } } - hasTicker(id: number): boolean { - return RenderItem.tickerMap.has(id); + removeExcitable(id: number, callEnd: boolean = true): boolean { + if (!this.root) return false; + else { + const excitable = this.delegationsIdMap.get(id); + if (excitable !== void 0) this.delegationsMap.delete(excitable); + this.delegationsIdMap.delete(id); + return this.root.removeExcitable(id, callEnd); + } + } + + removeExcitableObject( + excitable: ExcitableDelegation, + callEnd?: boolean + ): boolean { + if (!this.root) return false; + else { + const num = this.delegationsMap.get(excitable); + if (num !== void 0) this.delegationsIdMap.delete(num); + this.delegationsMap.delete(excitable); + return this.root.removeExcitableObject(excitable, callEnd); + } + } + + hasExcitable(id: number): boolean { + if (!this.root) return false; + else return this.root?.hasExcitable(id); } //#endregion //#region 父子关系 - setRoot(item: RenderItem & IRenderTreeRoot) { - this._root?.disconnect(this); - this._root = item; - item.connect(item); + setRoot(item: IRenderTreeRoot | null) { + this.root?.disconnect(this); + this.root = item; + if (item) { + item.connect(item); + this.connected = true; + } else { + this.connected = false; + } + if (item) { + this.beforeFrames.forEach(v => item.requestBeforeFrame(v)); + this.afterFrames.forEach(v => item.requestAfterFrame(v)); + this.toDelegate.forEach(obj => { + const { excitable, time, end } = obj; + const id = item.delegateExcitable(excitable, time, end); + this.delegationsMap.set(excitable, id); + this.delegationsIdMap.set(id, excitable); + }); + this.beforeFrames.clear(); + this.afterFrames.clear(); + this.toDelegate.clear(); + } } - checkRoot(): RenderItem | null { - if (this._root) return this._root; - if (this.isRoot) return this; - let ele: RenderItem = this; + checkRoot() { + if (this.root) return; + if (this.isRoot) return; + let ele: IRenderItem = this; while (!ele.isRoot) { - if (ele._root) { - this._root = ele._root; - return this._root; + if (ele.root) { + ele = ele.root; + break; } - if (!ele._parent) { - return null; + if (!ele.parent) { + return; } else { - ele = ele._parent; + ele = ele.parent; } } - this._root = ele as RenderItem & IRenderTreeRoot; - return ele; - } - - /** - * 刷新所有子元素 - */ - refreshAllChildren() { - if (this.children.size > 0) { - const stack: RenderItem[] = [this]; - while (stack.length > 0) { - const item = stack.pop(); - if (!item) continue; - item.cacheDirty = true; - item.children.forEach(v => stack.push(v)); - } - } - this.update(this); + this.setRoot(ele as IRenderTreeRoot); } /** * 将这个渲染元素添加到其他父元素上 * @param parent 父元素 */ - appendTo(parent: RenderItem) { + appendTo(parent: IRenderItem) { this.remove(); + const p = parent as RenderItem; parent.children.add(this); - this._parent = parent; - parent.requestSort(); + this.parent = p; + p.requestSort(); this.update(); this.checkRoot(); - this._root?.connect(this); - this._transform.bind(this); + this.transform.bind(this); this.onResize(parent.scale); + this.emit('resize', this.width, this.height); } /** @@ -793,15 +605,25 @@ export abstract class RenderItem */ remove(): boolean { if (!this.parent) return false; + this.delegationsIdMap.forEach(v => + this.root?.removeExcitableObject(v, false) + ); + this.delegationsMap.forEach(v => { + this.root?.removeExcitable(v, false); + }); + this.delegationsIdMap.clear(); + this.delegationsMap.clear(); + this.toDelegate.clear(); + this.beforeFrames.clear(); + this.afterFrames.clear(); const parent = this.parent; const success = parent.children.delete(this); - this._parent = void 0; + this.parent = null; parent.requestSort(); parent.update(); - this._transform.unbind(this); + this.transform.unbind(this); if (!success) return false; - this._root?.disconnect(this); - this._root = void 0; + this.setRoot(null); return true; } @@ -809,7 +631,7 @@ export abstract class RenderItem * 添加子元素,默认没有任何行为且会抛出警告,你需要在自己的RenderItem继承类中复写它,才可以使用 * @param child 子元素 */ - appendChild(..._child: RenderItem[]): void { + appendChild(..._child: IRenderItem[]): void { logger.warn(35); } @@ -817,7 +639,7 @@ export abstract class RenderItem * 移除子元素,默认没有任何行为且会抛出警告,你需要在自己的RenderItem继承类中复写它,才可以使用 * @param child 子元素 */ - removeChild(..._child: RenderItem[]): void { + removeChild(..._child: IRenderItem[]): void { logger.warn(36); } @@ -828,10 +650,26 @@ export abstract class RenderItem logger.warn(37); } + /** + * 获取排序后的子元素,根据 `zIndex` 排序,小的在前,大的在后。 + * 包含子元素的元素应该 `override` 此方法。 + */ + getSortedChildren(): IRenderItem[] { + return []; + } + //#endregion //#region 交互事件 + /** + * 设置是否忽略交互事件,交互事件将会直接下穿至在其下方的元素 + * @param ignore 是否忽略事件 + */ + ignoreEvent(ignore: boolean): void { + this.noEvent = ignore; + } + /** * 根据事件类型和事件阶段获取事件名称 * @param type 事件类型 @@ -919,10 +757,7 @@ export abstract class RenderItem if (this.noEvent) return null; if (progress === EventProgress.Capture) { // 捕获阶段需要计算鼠标位置 - const tran = this.transformFallThrough - ? this.fallTransform - : this._transform; - if (!tran) return null; + const tran = this.transform; const [nx, ny] = this.calActionPosition(event, tran); const inElement = this.isActionInElement(nx, ny); // 在元素范围内,执行事件 @@ -962,7 +797,7 @@ export abstract class RenderItem switch (type) { case ActionType.Move: { if (inElement) { - this._root?.hoverElement(this); + this.root?.hoverElement(this); } break; } @@ -1028,15 +863,8 @@ export abstract class RenderItem ): vec3 { const ax = this.anchorX * this.width; const ay = this.anchorY * this.height; - if (this.type === 'absolute') { - return [event.offsetX + ax, event.offsetY + ay, 0]; - } else { - const [tx, ty] = transform.untransformed( - event.offsetX, - event.offsetY - ); - return [tx + ax, ty + ay, 0]; - } + const [tx, ty] = transform.untransformed(event.offsetX, event.offsetY); + return [tx + ax, ty + ay, 0]; } /** @@ -1048,20 +876,6 @@ export abstract class RenderItem return x >= 0 && x < this.width && y >= 0 && y < this.height; } - actionClick() {} - - actionDown() {} - - actionUp() {} - - actionMove() {} - - actionEnter() {} - - actionLeave() {} - - actionWheel() {} - //#endregion //#region vue支持 props处理 @@ -1115,24 +929,23 @@ export abstract class RenderItem /** * 解析事件key * @param key 键名 - * @returns 返回字符串表示解析后的键名,返回布尔值表示不是事件 + * @returns 返回字符串表示解析后的键名,返回空字符串说明没有对应的事件 */ - protected parseEvent(key: string): string | false { + protected parseEvent(key: string): string { if (key.startsWith('on')) { const code = key.charCodeAt(2); if (code >= 65 && code <= 90) { return key[2].toLowerCase() + key.slice(3); } } - return false; + return ''; } /** - * 自定义处理 props,自定义元素需要 override 此函数来处理 props - * @param key 传入的 props 的键名 - * @param prevValue 这个 props 之前的值 - * @param nextValue 这个 props 传入的值 - * @returns 是否处理成功 + * `override` 以自定义 `vue` 参数处理,返回 false 以避免后续判断提高运行速度。 + * @param _key 属性键名 + * @param _prevValue 该属性先前的数值 + * @param _nextValue 该属性当前的数值 */ protected handleProps( _key: string, @@ -1142,6 +955,14 @@ export abstract class RenderItem return false; } + /** + * 在 jsx, vue 中当属性改变后触发此函数,用于处理响应式等情况 + * @param key 属性键名 + * @param prevValue 该属性先前的数值 + * @param nextValue 该属性当前的数值 + * @param _namespace 元素命名空间 + * @param _parentComponent 元素的父组件 + */ patchProp( key: string, prevValue: any, @@ -1154,12 +975,12 @@ export abstract class RenderItem switch (key) { case 'x': { if (!this.assertType(nextValue, 'number', key)) return; - this.pos(nextValue, this._transform.y); + this.pos(nextValue, this.transform.y); return; } case 'y': { if (!this.assertType(nextValue, 'number', key)) return; - this.pos(this._transform.x, nextValue); + this.pos(this.transform.x, nextValue); return; } case 'anchorX': { @@ -1220,8 +1041,8 @@ export abstract class RenderItem return; } case 'type': { - if (!this.assertType(nextValue, 'string', key)) return; - this.type = nextValue; + if (!this.assertType(nextValue, 'number', key)) return; + this.position = nextValue; this.update(); return; } @@ -1265,13 +1086,13 @@ export abstract class RenderItem } case 'cursor': { if (!this.assertType(nextValue, 'string', key)) return; - this.cursor = nextValue; + this.setCursor(nextValue); return; } case 'scale': { if (isEqual(nextValue, prevValue)) return; if (!this.assertType(nextValue, Array, key)) return; - this._transform.setScale( + this.transform.setScale( nextValue[0] as number, nextValue[1] as number ); @@ -1279,7 +1100,7 @@ export abstract class RenderItem } case 'rotate': { if (!this.assertType(nextValue, 'number', key)) return; - this._transform.setRotate(nextValue); + this.transform.setRotate(nextValue); return; } case 'noevent': { @@ -1289,11 +1110,11 @@ export abstract class RenderItem } } const ev = this.parseEvent(key); - if (ev) { + if (ev.length > 0) { if (prevValue) { - this.off(ev as keyof ERenderItemEvent, prevValue); + this.off(ev as keyof ERenderItemActionEvent, prevValue); } - this.on(ev as keyof ERenderItemEvent, nextValue); + this.on(ev as keyof ERenderItemActionEvent, nextValue); } } @@ -1304,38 +1125,14 @@ export abstract class RenderItem */ destroy(): void { this.remove(); - this.emit('destroy'); this.removeAllListeners(); this.canvases.clear(); - this.cache.clear(); + this.cache?.clear(); this.propagationStoped.clear(); this.cachedEvent.clear(); this.mouseId.clear(); this.touchId.clear(); this.children.clear(); - this._root = void 0; - this._parent = void 0; + this.setRoot(null); } } - -RenderItem.ticker.add(time => { - // slice 是为了让函数里面的 request 进入下一帧执行 - if (beforeFrame.length > 0) { - const arr = beforeFrame.slice(); - beforeFrame.splice(0); - arr.forEach(v => v()); - } - RenderItem.tickerMap.forEach(v => { - v.fn(time); - }); - if (renderFrame.length > 0) { - const arr = renderFrame.slice(); - renderFrame.splice(0); - arr.forEach(v => v()); - } - if (afterFrame.length > 0) { - const arr = afterFrame.slice(); - afterFrame.splice(0); - arr.forEach(v => v()); - } -}); diff --git a/packages/render/src/elements/misc.ts b/packages/render/src/core/misc.ts similarity index 85% rename from packages/render/src/elements/misc.ts rename to packages/render/src/core/misc.ts index e604623..6c40b14 100644 --- a/packages/render/src/elements/misc.ts +++ b/packages/render/src/core/misc.ts @@ -1,21 +1,12 @@ -import { - ERenderItemEvent, - RenderItem, - RenderItemPosition, - Transform, - MotaOffscreenCanvas2D -} from '../core'; +import { RenderItem, Transform, MotaOffscreenCanvas2D } from '.'; import { CanvasStyle } from '../types'; import { Font } from '../style'; +import { IRenderImage, IRenderText } from './types'; /** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */ const SAFE_PAD = 1; -export interface ETextEvent extends ERenderItemEvent { - setText: [text: string, width: number, height: number]; -} - -export class Text extends RenderItem { +export class Text extends RenderItem implements IRenderText { text: string; fillStyle?: CanvasStyle = '#fff'; @@ -28,14 +19,13 @@ export class Text extends RenderItem { private static measureCanvas = new MotaOffscreenCanvas2D(); - constructor(text: string = '', type: RenderItemPosition = 'static') { - super(type, false); + constructor(text: string = '', enableCache: boolean = false) { + super(enableCache); this.text = text; if (text.length > 0) { this.requestBeforeFrame(() => { this.calBox(); - this.emit('setText', text, this.width, this.height); }); } } @@ -79,8 +69,7 @@ export class Text extends RenderItem { setText(text: string) { this.text = text; this.calBox(); - this.update(this); - this.emit('setText', text, this.width, this.height); + this.update(); } /** @@ -157,13 +146,11 @@ export class Text extends RenderItem { } } -export interface EImageEvent extends ERenderItemEvent {} - -export class Image extends RenderItem { +export class Image extends RenderItem implements IRenderImage { image: CanvasImageSource; - constructor(image: CanvasImageSource, type: RenderItemPosition = 'static') { - super(type, false); + constructor(image: CanvasImageSource, enableCache: boolean = false) { + super(enableCache); this.image = image; if (image instanceof VideoFrame || image instanceof SVGElement) { this.size(200, 200); @@ -207,7 +194,7 @@ export class Comment extends RenderItem { readonly isComment: boolean = true; constructor(public text: string = '') { - super('static', false, false); + super(false); this.hide(); } @@ -219,8 +206,4 @@ export class Comment extends RenderItem { _canvas: MotaOffscreenCanvas2D, _transform: Transform ): void {} - - protected handleProps(): boolean { - return false; - } } diff --git a/packages/render/src/core/render.ts b/packages/render/src/core/render.ts index 16668b7..f471440 100644 --- a/packages/render/src/core/render.ts +++ b/packages/render/src/core/render.ts @@ -1,6 +1,6 @@ import { logger } from '@motajs/common'; import { MotaOffscreenCanvas2D } from './canvas2d'; -import { Container } from './container'; +import { Container, CustomContainer } from './container'; import { ActionType, IActionEvent, @@ -9,41 +9,68 @@ import { MouseType, WheelType } from './event'; -import { IRenderTreeRoot, RenderItem } from './item'; -import { Transform } from './transform'; +import { + IMotaRendererConfig, + RenderItemTags, + IRenderItem, + IRenderItemInstanceMap, + IRenderItemParameterMap, + IRenderTreeRoot, + RenderItemConstructor, + ExcitableDelegation +} from './types'; +import { IExcitable, IExcitation, RafExcitation } from '@motajs/animate'; +import { CustomRenderItem } from './custom'; +import { Comment, Image, Text } from './misc'; +import { Shader } from './shader'; +import { + BezierCurve, + Circle, + Ellipse, + Line, + Path, + QuadraticCurve, + Rect, + RectR +} from './graphics'; interface TouchInfo { /** 这次触摸在渲染系统的标识符 */ - identifier: number; + readonly identifier: number; /** 浏览器的 clientX,用于判断这个触点有没有移动 */ - clientX: number; + readonly clientX: number; /** 浏览器的 clientY,用于判断这个触点有没有移动 */ - clientY: number; + readonly clientY: number; /** 是否覆盖在了当前元素上 */ - hovered: boolean; + readonly hovered: boolean; } interface MouseInfo { /** 这个鼠标按键的标识符 */ - identifier: number; + readonly identifier: number; } -export interface MotaRendererConfig { - /** 要挂载到哪个画布上,可以填 css 选择器或画布元素本身 */ - canvas: string | HTMLCanvasElement; - /** 画布的宽度,所有渲染操作会自行适配缩放 */ - width: number; - /** 画布的高度,所有渲染操作会自行适配缩放 */ - height: number; - /** 是否启用不透明度通道,默认启用 */ - alpha?: boolean; +interface DelegatedExcitable extends IExcitable { + /** 委托内容的 id */ + readonly id: number; + /** 委托内容的原始对象 */ + readonly obj: ExcitableDelegation; + /** 委托 excitable 是否永久执行 */ + readonly forever: boolean; + /** 委托 excitable 的开始时刻 */ + readonly startTime: number; + /** 委托 excitable 的持续时间 */ + readonly time: number; + /** 委托结束时执行的函数 */ + readonly end?: () => void; } -export class MotaRenderer extends Container implements IRenderTreeRoot { - static list: Map = new Map(); - +export class MotaRenderer + extends Container + implements IRenderTreeRoot, IExcitable +{ /** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */ - protected idMap: Map = new Map(); + protected readonly idMap: Map = new Map(); /** 最后一次按下的鼠标按键,用于处理鼠标移动 */ private lastMouse: MouseType = MouseType.None; @@ -61,16 +88,40 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { /** 根据捕获行为判断光标样式 */ private targetCursor: string = 'auto'; /** 当前鼠标覆盖的元素 */ - private hoveredElement: Set = new Set(); + private hoveredElement: Set = new Set(); /** 本次交互前鼠标覆盖的元素 */ - private beforeHovered: Set = new Set(); - - target!: MotaOffscreenCanvas2D; + private beforeHovered: Set = new Set(); + /** 渲染至的目标画布 */ + readonly target!: MotaOffscreenCanvas2D; + /** 当前元素是根元素 */ readonly isRoot = true; - constructor(config: MotaRendererConfig) { - super('static', false); + /** 当前元素的激励源 */ + readonly excitation!: IExcitation; + + /** 下一帧之前需要执行的内容 */ + private readonly beforeFrame: Set<() => void> = new Set(); + /** 下一帧之后需要执行的内容 */ + private readonly afterFrame: Set<() => void> = new Set(); + /** 委托 excitables 的计数器 */ + private delegationCounter: number = 0; + /** 委托执行的 excitables */ + private readonly excitables: Map = new Map(); + /** 委托执行的 excitables 到其 id 的映射 */ + private readonly excitablesMap: Map< + ExcitableDelegation, + DelegatedExcitable + > = new Map(); + /** 执行完毕需要删除的 excitables */ + private readonly toDeleteExcitables: Set = new Set(); + + /** 标签注册信息 */ + private readonly tagRegistry: Map = + new Map(); + + constructor(config: IMotaRendererConfig) { + super(false); const canvas = this.getMountCanvas(config.canvas); if (!canvas) { @@ -80,24 +131,132 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { this.target = new MotaOffscreenCanvas2D(config.alpha ?? true, canvas); this.size(config.width, config.height); this.target.setAntiAliasing(false); + if (config.excitaion) { + this.excitation = config.excitaion; + } else { + this.excitation = new RafExcitation(); + } 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); + this.excited = this.excited.bind(this); + this.excitation.add(this); + this.registerIntrinsicTags(); } + //#region 渲染相关 + + excited(payload: number): void { + this.beforeFrame.forEach(v => v()); + this.beforeFrame.clear(); + this.refresh(); + this.afterFrame.forEach(v => v()); + this.afterFrame.clear(); + + this.excitables.forEach((ex, key) => { + if (!ex.forever && payload - ex.startTime >= ex.time) { + this.toDeleteExcitables.add(key); + ex.end?.(); + } else { + ex.excited(payload); + } + }); + this.toDeleteExcitables.forEach(key => this.excitables.delete(key)); + } + + private getMountCanvas( + canvas: string | HTMLCanvasElement + ): HTMLCanvasElement | undefined { + if (typeof canvas === 'string') { + return document.querySelector(canvas) as HTMLCanvasElement; + } else { + return canvas; + } + } + + update(_item: IRenderItem = this) { + this.cacheDirty = true; + } + + protected refresh(): void { + if (!this.cacheDirty) return; + this.target.clear(); + this.renderContent(this.target); + } + + getCanvas(): HTMLCanvasElement { + return this.target.canvas; + } + + //#endregion + + //#region 标签元素 + + createElement( + tag: K, + ...params: IRenderItemParameterMap[K] + ): IRenderItemInstanceMap[K]; + createElement

( + ele: new (...params: P) => I, + ...params: P + ): I; + createElement( + ele: RenderItemTags | RenderItemConstructor, + ...params: any[] + ): IRenderItem { + if (typeof ele === 'string') { + const Cons = this.tagRegistry.get(ele); + if (!Cons) { + logger.error(15, ele); + return new Container(false); + } else { + return new Cons(...params); + } + } else { + return new ele(...params); + } + } + + registerElement(tag: string, cons: RenderItemConstructor): void { + if (this.tagRegistry.has(tag)) { + logger.error(14); + return; + } else { + this.tagRegistry.set(tag, cons); + } + } + + hasTag(tag: string): boolean { + return this.tagRegistry.has(tag); + } + + /** + * 注册内置渲染标签 + */ + private registerIntrinsicTags() { + this.registerElement('container', Container); + this.registerElement('custom', CustomRenderItem); + this.registerElement('text', Text); + this.registerElement('image', Image); + this.registerElement('shader', Shader); + this.registerElement('comment', Comment); + this.registerElement('template', Container); + this.registerElement('custom-container', CustomContainer); + this.registerElement('g-rect', Rect); + this.registerElement('g-circle', Circle); + this.registerElement('g-ellipse', Ellipse); + this.registerElement('g-line', Line); + this.registerElement('g-bezier', BezierCurve); + this.registerElement('g-quad', QuadraticCurve); + this.registerElement('g-path', Path); + this.registerElement('g-rectr', RectR); + } + + //#endregion + + //#region 尺寸缩放 + /** * 设置这个渲染器的缩放比 * @param scale 缩放比 @@ -106,6 +265,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { this.onResize(scale); } + getScale() { + return this.target.scale; + } + onResize(scale: number): void { this.target.setScale(scale); const width = this.target.width * scale; @@ -115,23 +278,20 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { 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); } + //#endregion + + //#region 事件处理 + private listen() { // 画布监听 const canvas = this.target.canvas; + canvas.addEventListener('mousedown', ev => { const mouse = this.getMouseType(ev); this.lastMouse = mouse; @@ -178,47 +338,6 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { 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, @@ -240,6 +359,70 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { document.addEventListener('click', clear, { signal }); document.addEventListener('mouseenter', clear, { signal }); document.addEventListener('mouseleave', clear, { signal }); + window.addEventListener( + 'resize', + () => { + this.requestAfterFrame(() => this.refreshAllChildren()); + }, + { signal } + ); + document.addEventListener( + 'touchstart', + ev => { + this.createTouchAction(ev, ActionType.Down).forEach(v => { + this.captureEvent(ActionType.Down, v); + }); + }, + { signal } + ); + 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); + }); + }, + { signal } + ); + 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); + }); + }, + { signal } + ); + 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 + ); + }); + }, + { signal } + ); } private isTouchInCanvas(clientX: number, clientY: number) { @@ -312,7 +495,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { private createMouseActionBase( event: MouseEvent, id: number, - target: RenderItem = this, + target: IRenderItem = this, mouse: MouseType = this.getMouseType(event) ): IActionEventBase { return { @@ -331,7 +514,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { private createTouchActionBase( event: TouchEvent, id: number, - target: RenderItem + target: IRenderItem ): IActionEventBase { return { identifier: id, @@ -488,8 +671,8 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { private checkMouseEnterLeave( event: MouseEvent, ev: IActionEvent, - before: Set, - now: Set + before: Set, + now: Set ) { // 先 leave,再 enter before.forEach(v => { @@ -513,8 +696,8 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { private checkTouchEnterLeave( event: TouchEvent, ev: IActionEvent, - before: Set, - now: Set + before: Set, + now: Set ) { // 先 leave,再 enter before.forEach(v => { @@ -535,22 +718,16 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { }); } - update(_item: RenderItem = this) { - this.cacheDirty = true; - } + //#endregion - protected refresh(): void { - if (!this.cacheDirty) return; - this.target.clear(); - this.renderContent(this.target, Transform.identity); - } + //#region 元素处理 /** * 根据渲染元素的id获取一个渲染元素 * @param id 要获取的渲染元素id * @returns */ - getElementById(id: string): RenderItem | null { + getElementById(id: string): IRenderItem | null { if (id.length === 0) return null; const item = this.idMap.get(id); if (item) return item; @@ -563,7 +740,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { } } - private searchElement(ele: RenderItem, id: string): RenderItem | null { + private searchElement(ele: IRenderItem, id: string): IRenderItem | null { for (const child of ele.children) { if (child.id === id) return child; else { @@ -574,7 +751,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { return null; } - connect(item: RenderItem): void { + connect(item: IRenderItem): void { if (item.id.length === 0) return; const existed = this.idMap.get(item.id); if (existed) { @@ -585,11 +762,11 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { } } - disconnect(item: RenderItem): void { + disconnect(item: IRenderItem): void { this.idMap.delete(item.id); } - modifyId(item: RenderItem, previous: string, current: string): void { + modifyId(item: IRenderItem, previous: string, current: string): void { this.idMap.delete(previous); if (current.length !== 0) { if (this.idMap.has(item.id)) { @@ -600,24 +777,93 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { } } - getCanvas(): HTMLCanvasElement { - return this.target.canvas; - } - - hoverElement(element: RenderItem): void { + hoverElement(element: IRenderItem): void { if (element.cursor !== 'inherit') { this.targetCursor = element.cursor; } this.hoveredElement.add(element); } + //#endregion + + //#region 渲染绑定 + + requestBeforeFrame(fn: () => void): void { + this.beforeFrame.add(fn); + } + + requestAfterFrame(fn: () => void): void { + this.afterFrame.add(fn); + } + + delegateExcitable( + fn: ExcitableDelegation, + time?: number, + end?: () => void + ): number { + const index = this.delegationCounter++; + const info: DelegatedExcitable = { + id: index, + obj: fn, + excited: typeof fn === 'function' ? fn : fn.excited, + startTime: this.excitation.payload(), + time: time ?? 0, + forever: time === void 0, + end + }; + this.excitables.set(index, info); + this.excitablesMap.set(fn, info); + return index; + } + + removeExcitable(id: number, callEnd: boolean = true): boolean { + const info = this.excitables.get(id); + if (!info) return false; + if (callEnd) { + info.end?.(); + } + this.excitables.delete(id); + this.excitablesMap.delete(info.obj); + return true; + } + + removeExcitableObject( + excitable: ExcitableDelegation, + callEnd: boolean = true + ): boolean { + const info = this.excitablesMap.get(excitable); + if (!info) return false; + if (callEnd) { + info.end?.(); + } + this.excitables.delete(info.id); + this.excitablesMap.delete(info.obj); + return true; + } + + hasExcitable(id: number): boolean { + return this.excitables.has(id); + } + + //#endregion + + //#region 元素销毁 + destroy() { super.destroy(); - MotaRenderer.list.delete(this.id); + this.excitation.destroy(); this.abort?.abort(); } - private toTagString(item: RenderItem, space: number, deep: number): string { + //#endregion + + //#region 调试功能 + + private toTagString( + item: IRenderItem, + space: number, + deep: number + ): string { if (item.isComment) return ''; const name = item.constructor.name; if (item.children.size === 0) { @@ -647,18 +893,5 @@ export class MotaRenderer extends Container implements IRenderTreeRoot { return this.toTagString(this, space, 0); } - static get(id: string) { - return this.list.get(id); - } + //#endregion } - -window.addEventListener('resize', () => { - MotaRenderer.list.forEach(v => - v.requestAfterFrame(() => v.refreshAllChildren()) - ); -}); - -// @ts-expect-error debug -window.logTagTree = () => { - console.log(MotaRenderer.get('render-main')?.toTagTree()); -}; diff --git a/packages/render/src/core/shader.ts b/packages/render/src/core/shader.ts index cb6b315..7f1d3e5 100644 --- a/packages/render/src/core/shader.ts +++ b/packages/render/src/core/shader.ts @@ -1,6 +1,6 @@ import { MotaOffscreenCanvas2D } from './canvas2d'; -import { EGL2Event, GL2, GL2Program, IGL2ProgramPrefix } from './gl2'; -import { RenderItemPosition } from './item'; +import { GL2, GL2Program } from './gl2'; +import { IGL2ProgramPrefix, IWebGL2RenderItem } from './types'; const SHADER_PREFIX: IGL2ProgramPrefix = { VERTEX: /* glsl */ `#version 300 es @@ -34,15 +34,7 @@ void main() { } `; -export interface EShaderEvent extends EGL2Event {} - -export class Shader extends GL2< - EShaderEvent | E -> { - constructor(type: RenderItemPosition = 'static') { - super(type); - } - +export class Shader extends GL2 { protected drawScene( canvas: MotaOffscreenCanvas2D, gl: WebGL2RenderingContext @@ -57,7 +49,7 @@ export class Shader extends GL2< export class ShaderProgram extends GL2Program { protected readonly prefix: IGL2ProgramPrefix = SHADER_PREFIX; - constructor(gl2: GL2, vs?: string, fs?: string) { + constructor(gl2: IWebGL2RenderItem, vs?: string, fs?: string) { super(gl2, vs, fs); if (!vs) this.vs(DEFAULT_VS); if (!fs) this.fs(DEFAULT_FS); diff --git a/packages/render/src/core/sprite.ts b/packages/render/src/core/sprite.ts deleted file mode 100644 index 63cf4ae..0000000 --- a/packages/render/src/core/sprite.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 { - 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; - } -} diff --git a/packages/render/src/core/types.ts b/packages/render/src/core/types.ts new file mode 100644 index 0000000..2e22654 --- /dev/null +++ b/packages/render/src/core/types.ts @@ -0,0 +1,1728 @@ +import { IExcitable, IExcitation } from '@motajs/animate'; +import { ITransformUpdatable, Transform } from './transform'; +import { + ActionEventMap, + ActionType, + ERenderItemActionEvent, + EventProgress +} from './event'; +import { MotaOffscreenCanvas2D } from './canvas2d'; +import { Font } from '../style'; +import { DefineComponent, DefineSetupFnComponent } from 'vue'; +import { JSX } from 'vue/jsx-runtime'; +import EventEmitter from 'eventemitter3'; +import { SizedCanvasImageSource } from '../types'; + +//#region 功能类型 + +export const enum RenderPosition { + /** 绝对定位,受到坐标与变换矩阵的影响 */ + Absolute +} + +export type Props< + T extends + | keyof JSX.IntrinsicElements + | DefineSetupFnComponent + | DefineComponent +> = T extends keyof JSX.IntrinsicElements + ? JSX.IntrinsicElements[T] + : T extends DefineSetupFnComponent + ? InstanceType['$props'] & InstanceType['$emits'] + : T extends DefineComponent + ? InstanceType['$props'] & InstanceType['$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]; + +//#endregion + +//#region 渲染元素 + +export type ExcitableDelegation = + | IExcitable + | ((payload: number) => void); + +export interface ERenderItemEvent extends ERenderItemActionEvent { + resize: [width: number, height: number]; + transform: [item: IRenderItem, transform: Transform]; +} + +export interface IRenderItem + extends ITransformUpdatable, EventEmitter { + /** 是否为根元素 */ + readonly isRoot: boolean; + /** 当前元素的父元素 */ + readonly parent: IRenderItem | null; + /** 当前元素的子元素 */ + readonly children: Set; + /** 此元素所附属的根元素 */ + readonly root: IRenderTreeRoot | null; + /** 当前元素是否连接至根元素 */ + readonly connected: boolean; + + /** 渲染元素的唯一标识符,每个渲染元素都会不一样 */ + readonly uid: number; + /** 渲染元素标识符,原则上不能重复 */ + readonly id: string; + + /** 是否是注释元素 */ + readonly isComment: boolean; + + /** 元素纵深 */ + readonly zIndex: number; + /** 元素横坐标 */ + readonly x: number; + /** 元素纵坐标 */ + readonly y: number; + /** 元素宽度 */ + readonly width: number; + /** 元素高度 */ + readonly height: number; + /** 锚点横坐标,0表示最左端,1表示最右端 */ + readonly anchorX: number; + /** 锚点纵坐标,0表示最上端,1表示最下端 */ + readonly anchorY: number; + + /** 元素定位方式 */ + readonly position: RenderPosition; + /** 元素变换矩阵 */ + readonly transform: Transform; + + /** 是否是高清画布 */ + readonly highResolution: boolean; + /** 是否启用抗锯齿 */ + readonly antiAliasing: boolean; + /** 是否隐藏当前元素,隐藏后不会被渲染,但仍存在于渲染树中 */ + readonly hidden: boolean; + /** 元素滤镜 */ + readonly filter: string; + /** 元素与在其下方的元素的混合模式 */ + readonly composite: GlobalCompositeOperation; + /** 元素不透明度 */ + readonly alpha: number; + /** 元素的缩放比例,与渲染器的缩放比例及 `devicePixelRatio` 等有关 */ + readonly scale: number; + + /** 光标样式 */ + readonly cursor: string; + /** 当前元素是否忽略交互事件 */ + readonly noEvent: boolean; + + /** 当前元素是否启用缓存机制 */ + readonly enableCache: boolean; + + //#region 渲染部分 + + /** + * 更新这个渲染元素 + * @param item 触发更新事件的元素,不填默认为元素自身触发 + */ + update(item?: IRenderItem): void; + + /** + * 刷新所有子元素 + */ + refreshAllChildren(): void; + + /** + * 将此元素渲染至目标画布上 + * @param canvas 渲染至的画布 + * @param transform 父元素的变换矩阵 + */ + renderContent(canvas: MotaOffscreenCanvas2D, transform: Transform): void; + + //#endregion + + //#region 画布方法 + + /** + * 申请一个 `MotaOffscreenCanvas2D`,即申请一个画布,画布将会存储在元素身上。 + * 为确保不会内存泄漏,当画布不会再被使用时请及时调用 {@link deleteCanvas} 退还。 + * @param alpha 是否启用画布的 alpha 通道 + * @param autoScale 是否自动跟随缩放 + */ + requireCanvas(alpha?: boolean, autoScale?: boolean): MotaOffscreenCanvas2D; + + /** + * 删除由 `requireCanvas` 申请的画布,当画布不再使用时,需要用该方法删除画布 + * @param canvas 要删除的画布 + */ + deleteCanvas(canvas: MotaOffscreenCanvas2D): void; + + //#endregion + + //#region 渲染设置 + + /** + * 修改这个对象的大小 + */ + size(width: number, height: number): void; + + /** + * 设置这个元素的位置,等效于`transform.setTranslate(x, y)` + * @param x 横坐标 + * @param y 纵坐标 + */ + pos(x: number, y: number): void; + + /** + * 设置元素的纵深 + * @param zIndex 元素纵深 + */ + setZIndex(zIndex: number): void; + + /** + * 设置当前渲染元素是否使用高清画布 + * @param hd 是否高清 + */ + setHD(hd: boolean): void; + + /** + * 设置当前渲染元素是否启用抗锯齿 + * @param anti 是否抗锯齿 + */ + setAntiAliasing(anti: boolean): void; + + /** + * 设置渲染元素的位置锚点 + * @param x 锚点的横坐标,小数,0表示最左边,1表示最右边 + * @param y 锚点的纵坐标,小数,0表示最上边,1表示最下边 + */ + setAnchor(x: number, y: number): void; + + /** + * 设置元素滤镜 + * @param filter 滤镜字符串 + */ + setFilter(filter: string): void; + + /** + * 设置不透明度 + * @param alpha 不透明度 + */ + setAlpha(alpha: number): void; + + /** + * 设置光标样式 + * @param cursor 光标样式 + */ + setCursor(cursor: string): void; + + /** + * 设置元素与其下方内容的混合模式 + * @param composite 混合模式 + */ + setComposite(composite: GlobalCompositeOperation): void; + + /** + * 设置元素变换矩阵 + * @param transform 变换矩阵对象 + */ + setTransform(transform: Transform): void; + + /** + * 隐藏此元素 + */ + hide(): void; + + /** + * 显示此元素 + */ + show(): void; + + //#endregion + + //#region 功能方法 + + /** + * 获取当前元素的绝对位置(不建议使用,因为应当很少会有获取绝对位置的需求) + * @param x 点相对于元素的横坐标 + * @param y 点相对于元素的纵坐标 + */ + getAbsolutePosition(x: number, y: number): [number, number]; + + /** + * 获取到可以包围这个元素的最小矩形,相对于父元素 + */ + getBoundingRect(): DOMRectReadOnly; + + /** + * 在下一帧渲染之前执行函数,常用于渲染前数据更新,理论上不应当用于渲染,不保证运行顺序 + * 此方法会将委托在根元素上,如果当前元素没有绑定任何根元素,那么不会有任何行为。 + * @param fn 执行的函数 + */ + requestBeforeFrame(fn: () => void): void; + + /** + * 在下一帧渲染之后执行函数,理论上不应当用于渲染,不保证运行顺序 + * 此方法会将委托在根元素上,如果当前元素没有绑定任何根元素,那么不会有任何行为。 + * @param fn 执行的函数 + */ + requestAfterFrame(fn: () => void): void; + + /** + * 委托 Excitable,让其在指定时间范围内每帧执行对应函数,超过时间后自动删除。 + * 此方法会将委托在根元素的激励源上,如果此当前元素没有绑定任何根元素, + * 那么将会临时存储此激励对象,然后当此元素首次绑定在任何根元素上时,将会自动委托至目标根元素。 + * @param excitable 每帧执行的激励对象或函数 + * @param time 函数持续时间,不填代表不会自动删除,需要手动删除 + * @param end 持续时间结束后执行的函数 + * @returns 委托id,可用于删除 + */ + delegateExcitable( + excitable: ExcitableDelegation, + time?: number, + end?: () => void + ): number; + + /** + * 移除委托激励对象。此方法需要传入 {@link delegateExcitable} 的返回值,但是当元素未绑定根元素时, + * 虽然会在绑定时调用,但是你并不能获得对应的 id,因此更推荐使用 {@link removeExcitableObject}。 + * @param id 函数id,也就是{@link delegateExcitable}的返回值 + * @param callEnd 是否调用结束函数,即{@link delegateExcitable}的end参数,默认调用 + * @returns 是否删除成功,比如对应ticker不存在,就是删除失败 + */ + removeExcitable(id: number, callEnd?: boolean): boolean; + + /** + * 移除委托激励对象 + * @param excitable 委托激励对象或函数 + * @param callEnd 是否调用结束函数,默认调用 + */ + removeExcitableObject( + excitable: ExcitableDelegation, + callEnd?: boolean + ): boolean; + + /** + * 检查是否包含一个委托函数 + * @param id 函数id + */ + hasExcitable(id: number): boolean; + + //#endregion + + //#region 父子关系 + + /** + * 向这个元素添加子元素 + * @param child 添加的元素 + */ + appendChild(...child: IRenderItem[]): void; + + /** + * 移除这个元素中的某个子元素 + * @param child 要移除的元素 + */ + removeChild(...child: IRenderItem[]): void; + + /** + * 将这个渲染元素添加到其他父元素上 + * @param parent 父元素 + */ + appendTo(parent: IRenderItem): void; + + /** + * 从渲染树中移除这个节点 + * @returns 是否移除成功 + */ + remove(): boolean; + + /** + * 获取排序后的子元素,根据 `zIndex` 排序,小的在前,大的在后 + */ + getSortedChildren(): IRenderItem[]; + + //#endregion + + //#region 事件相关 + + /** + * 设置是否忽略交互事件,交互事件将会直接下穿至在其下方的元素 + * @param ignore 是否忽略事件 + */ + ignoreEvent(ignore: boolean): void; + + /** + * 根据事件类型和事件阶段获取事件名称 + * @param type 事件类型 + * @param progress 事件阶段 + */ + getEventName( + type: ActionType, + progress: EventProgress + ): keyof ERenderItemActionEvent; + + //#endregion + + //#region 其他接口 + + /** + * 设置元素的 id + * @param id 元素 id + */ + setID(id: string): void; + + /** + * 摧毁这个渲染元素,摧毁后不应继续使用 + */ + destroy(): void; + + //#endregion +} + +//#endregion + +//#region 根元素 + +export interface IRenderItemParameterMap { + container: [enableCache?: boolean]; + custom: [enableCache?: boolean]; + text: [text: string, enableCache?: boolean]; + image: [image: CanvasImageSource, enableCache?: boolean]; + shader: []; + comment: [text?: string]; + template: [enableCache?: boolean]; + 'custom-container': [enableCache?: boolean]; + 'g-rect': [enableCache?: boolean]; + 'g-circle': [enableCache?: boolean]; + 'g-ellipse': [enableCache?: boolean]; + 'g-line': [enableCache?: boolean]; + 'g-bezier': [enableCache?: boolean]; + 'g-quad': [enableCache?: boolean]; + 'g-path': [enableCache?: boolean]; + 'g-rectr': [enableCache?: boolean]; +} + +export interface IRenderItemInstanceMap { + container: IRenderItem; + custom: IRenderCustom; + text: IRenderText; + image: IRenderImage; + shader: IWebGL2RenderItem; + comment: IRenderItem; + template: IRenderItem; + 'custom-container': ICustomContainer; + 'g-rect': IGraphicRenderItem; + 'g-circle': IGraphicCircle; + 'g-ellipse': IGraphicEllipse; + 'g-line': IGraphicLine; + 'g-bezier': IGraphicBezierCurve; + 'g-quad': IGraphicQuadBezierCurve; + 'g-path': IGraphicPath; + 'g-rectr': IGraphicRectR; +} + +export type RenderItemTags = keyof IRenderItemParameterMap & + keyof IRenderItemInstanceMap; + +export interface IMotaRendererConfig { + /** 要挂载到哪个画布上,可以填 css 选择器或画布元素本身 */ + readonly canvas: string | HTMLCanvasElement; + /** 画布的宽度,所有渲染操作会自行适配缩放 */ + readonly width: number; + /** 画布的高度,所有渲染操作会自行适配缩放 */ + readonly height: number; + /** 是否启用不透明度通道,默认启用 */ + readonly alpha?: boolean; + /** 指定渲染器的激励源,默认使用 `RafExcitation` */ + readonly excitaion?: IExcitation; +} + +export type RenderItemConstructor = new (...params: any[]) => IRenderItem; + +export interface IRenderTreeRoot extends IRenderItem { + /** 是否是根元素 */ + readonly isRoot: true; + /** 根元素的激励源 */ + readonly excitation: IExcitation; + + /** + * 根据标签名称创建对应的元素 + * @param tag 标签名称 + * @param params 传给标签的参数 + */ + createElement( + tag: K, + ...params: IRenderItemParameterMap[K] + ): IRenderItemInstanceMap[K]; + /** + * 根据元素类创建对应的元素 + * @param ele 元素构造器 + * @param params 传递给构造器的参数 + */ + createElement

( + ele: new (...params: P) => I, + ...params: P + ): I; + + /** + * 注册标签渲染元素 + * @param tag 标签名称 + * @param cons 对应渲染元素的构造器 + */ + registerElement(tag: string, cons: RenderItemConstructor): void; + + /** + * 当前根元素是否包含指定标签 + * @param tag 标签名称 + */ + hasTag(tag: string): boolean; + + /** + * 将一个渲染元素连接到此根元素 + * @param item 要连接到此根元素的渲染元素 + */ + connect(item: IRenderItem): void; + + /** + * 将已连接的渲染元素从此根元素中去掉 + * @param item 要取消连接的渲染元素 + */ + disconnect(item: IRenderItem): void; + + /** + * 修改已连接的元素的 id + * @param item 修改了 id 的元素 + * @param previous 先前的元素 id + * @param current 现在的元素 id + */ + modifyId(item: IRenderItem, previous: string, current: string): void; + + /** + * 获取渲染至的目标画布,即显示在画面上的画布 + */ + getCanvas(): HTMLCanvasElement; + + /** + * 当鼠标覆盖在某个元素上时执行 + * @param element 鼠标覆盖的元素 + */ + hoverElement(element: IRenderItem): void; + + /** + * 设置这个渲染器的缩放比 + * @param scale 缩放比 + */ + setScale(scale: number): void; + + /** + * 获取当前渲染器的缩放比 + */ + getScale(): number; +} + +//#endregion + +//#region 核心元素 + +export interface IRenderText extends IRenderItem { + /** 文字内容 */ + readonly text: string; + /** 填充样式 */ + readonly fillStyle?: CanvasStyle; + /** 描边样式 */ + readonly strokeStyle?: CanvasStyle; + /** 文字字体 */ + readonly font: Font; + /** 描边宽度 */ + readonly strokeWidth: number; + + /** + * 获取文字的长度 + */ + measure(): TextMetrics; + + /** + * 设置显示文字 + * @param text 显示的文字 + */ + setText(text: string): void; + + /** + * 设置使用的字体 + * @param font 字体 + */ + setFont(font: Font): void; + + /** + * 设置字体样式 + * @param fill 填充样式 + * @param stroke 描边样式 + */ + setStyle(fill?: CanvasStyle, stroke?: CanvasStyle): void; + + /** + * 设置描边宽度 + * @param width 宽度 + */ + setStrokeWidth(width: number): void; +} + +export interface IRenderImage extends IRenderItem { + /** 当前元素的图片内容 */ + readonly image: CanvasImageSource; + + /** + * 设置图片资源 + * @param image 图片资源 + */ + setImage(image: CanvasImageSource): void; +} + +//#endregion + +//#region 自定义元素 + +export type CustomRenderFunction = (canvas: MotaOffscreenCanvas2D) => void; + +export interface IRenderCustom extends IRenderItem { + /** 当前自定义渲染元素的渲染函数 */ + readonly renderFn: CustomRenderFunction; + + /** + * 设置渲染函数 + * @param fn 渲染函数 + */ + setRenderFn(fn: CustomRenderFunction): void; +} + +export type CustomContainerRenderFn = ( + canvas: MotaOffscreenCanvas2D, + children: IRenderItem[], + transform: Transform +) => void; + +export type CustomContainerPropagateOrigin = ( + type: T, + progress: EventProgress, + event: ActionEventMap[T] +) => void; + +export type CustomContainerPropagateFn = ( + type: T, + progress: EventProgress, + event: ActionEventMap[T], + container: ICustomContainer, + origin: CustomContainerPropagateOrigin +) => void; + +export interface ICustomContainer extends IRenderItem { + /** + * 设置这个自定义容器的渲染函数 + * @param render 渲染函数 + */ + setRenderFn(fn: CustomContainerRenderFn): void; + + /** + * 设置这个自定义容器的事件传递函数 + * @param propagate 事件传递函数 + */ + setPropagateFn(fn: CustomContainerPropagateFn): void; +} + +//#endregion + +//#region 图形元素 + +export type CircleParams = [ + cx?: number, + cy?: number, + radius?: number, + start?: number, + end?: number +]; +export type EllipseParams = [ + cx?: number, + cy?: number, + radiusX?: number, + radiusY?: number, + start?: number, + end?: number +]; +export type LineParams = [x1: number, y1: number, x2: number, y2: number]; +export type BezierParams = [ + sx: number, + sy: number, + cp1x: number, + cp1y: number, + cp2x: number, + cp2y: number, + ex: number, + ey: number +]; +export type QuadParams = [ + sx: number, + sy: number, + cpx: number, + cpy: number, + ex: number, + ey: number +]; +export type RectRCircleParams = [ + r1: number, + r2?: number, + r3?: number, + r4?: number +]; +export type RectREllipseParams = [ + rx1: number, + ry1: number, + rx2?: number, + ry2?: number, + rx3?: number, + ry3?: number, + rx4?: number, + ry4?: number +]; + +export interface ILineProperty { + /** 线宽 */ + readonly lineWidth: number; + /** 线的虚线设置 */ + readonly lineDash?: number[]; + /** 虚线偏移量 */ + readonly lineDashOffset?: number; + /** 线的连接样式 */ + readonly lineJoin: CanvasLineJoin; + /** 线的顶端样式 */ + readonly lineCap: CanvasLineCap; + /** 线的斜接限制,当连接为miter类型时可填,默认为10 */ + readonly miterLimit: number; +} + +export interface IGraphicProperty extends ILineProperty { + /** 渲染模式,参考 {@link GraphicMode} */ + readonly mode: GraphicMode; + /** 填充样式 */ + readonly fill: CanvasStyle; + /** 描边样式 */ + readonly stroke: CanvasStyle; + /** 填充算法 */ + readonly fillRule: CanvasFillRule; +} + +export const enum GraphicMode { + /** 仅填充 */ + Fill, + /** 仅描边 */ + Stroke, + /** 先填充,然后描边 */ + FillAndStroke, + /** 先描边,然后填充 */ + StrokeAndFill +} + +export interface IGraphicRenderItem extends IRenderItem { + /** + * 设置描边绘制的信息 + * @param options 线的信息 + */ + setLineOption(options: Partial): void; + + /** + * 设置填充样式 + * @param style 绘制样式 + */ + setFillStyle(style: CanvasStyle): void; + + /** + * 设置描边样式 + * @param style 绘制样式 + */ + setStrokeStyle(style: CanvasStyle): void; + + /** + * 设置填充原则 + * @param rule 填充原则 + */ + setFillRule(rule: CanvasFillRule): void; + + /** + * 设置绘制模式,是描边还是填充 + * @param mode 绘制模式 + */ + setMode(mode: GraphicMode): void; +} + +export interface IGraphicCircle extends IGraphicRenderItem { + /** 圆半径 */ + readonly radius: number; + /** 圆的起始角度 */ + readonly start: number; + /** 圆的终止角度 */ + readonly end: number; + + /** + * 设置圆的半径 + * @param radius 半径 + */ + setRadius(radius: number): void; + + /** + * 设置圆的起始与终止角度 + * @param start 起始角度 + * @param end 终止角度 + */ + setAngle(start: number, end: number): void; +} + +export interface IGraphicEllipse extends IGraphicRenderItem { + /** 椭圆横轴长 */ + readonly radiusX: number; + /** 椭圆纵轴长 */ + readonly radiusY: number; + /** 椭圆起始角度 */ + readonly start: number; + /** 椭圆终止角度 */ + readonly end: number; + + /** + * 设置椭圆的横纵轴长度 + * @param x 横轴长度 + * @param y 纵轴长度 + */ + setRadius(x: number, y: number): void; + + /** + * 设置椭圆的起始与终止角度 + * @param start 起始角度 + * @param end 终止角度 + */ + setAngle(start: number, end: number): void; +} + +export interface IGraphicLine extends IGraphicRenderItem { + /** 起始点横坐标 */ + readonly x1: number; + /** 起始点纵坐标 */ + readonly y1: number; + /** 终止点横坐标 */ + readonly x2: number; + /** 终止点纵坐标 */ + readonly y2: number; + + /** + * 设置第一个点的横纵坐标 + */ + setPoint1(x: number, y: number): void; + + /** + * 设置第二个点的横纵坐标 + */ + setPoint2(x: number, y: number): void; +} + +export interface IGraphicBezierCurve extends IGraphicRenderItem { + /** 起始点横坐标 */ + readonly sx: number; + /** 起始点纵坐标 */ + readonly sy: number; + /** 控制点1横坐标 */ + readonly cp1x: number; + /** 控制点1纵坐标 */ + readonly cp1y: number; + /** 控制点2横坐标 */ + readonly cp2x: number; + /** 控制点2纵坐标 */ + readonly cp2y: number; + /** 终止点横坐标 */ + readonly ex: number; + /** 终止点纵坐标 */ + readonly ey: number; + + /** + * 设置起始点坐标 + */ + setStart(x: number, y: number): void; + + /** + * 设置控制点1坐标 + */ + setControl1(x: number, y: number): void; + + /** + * 设置控制点2坐标 + */ + setControl2(x: number, y: number): void; + + /** + * 设置终点坐标 + */ + setEnd(x: number, y: number): void; +} + +export interface IGraphicQuadBezierCurve extends IGraphicRenderItem { + /** 起始点横坐标 */ + readonly sx: number; + /** 起始点纵坐标 */ + readonly sy: number; + /** 控制点横坐标 */ + readonly cpx: number; + /** 控制点纵坐标 */ + readonly cpy: number; + /** 终止点横坐标 */ + readonly ex: number; + /** 终止点纵坐标 */ + readonly ey: number; + + /** + * 设置起始点坐标 + */ + setStart(x: number, y: number): void; + + /** + * 设置控制点坐标 + */ + setControl(x: number, y: number): void; + + /** + * 设置终点坐标 + */ + setEnd(x: number, y: number): void; +} + +export interface IGraphicPath extends IGraphicRenderItem { + /** 当前元素的路径对象 */ + readonly path: Path2D; + + /** + * 获取当前元素的路径对象 + */ + getPath(): Path2D; + + /** + * 重置此元素的路径 + */ + resetPath(): void; + + /** + * 为路径添加路径 + * @param path 要添加的路径 + */ + addPath(path: Path2D): void; +} + +export const enum RectRCorner { + TopLeft, + TopRight, + BottomRight, + BottomLeft +} + +export interface IGraphicRectR extends IGraphicRenderItem { + /** 圆角属性,四元素数组,每个元素是一个二元素数组,表示这个角的半径,顺序为 左上,右上,右下,左下 */ + readonly corner: [radiusX: number, radiusY: number][]; + + /** + * 设置圆角半径 + * @param x 横向半径 + * @param y 纵向半径 + */ + setRadius(x: number, y: number, corner: RectRCorner): void; + + /** + * 设置圆形圆角参数 + * @param circle 圆形圆角参数 + */ + setCircle(circle: RectRCircleParams): void; + + /** + * 设置椭圆圆角参数 + * @param ellipse 椭圆圆角参数 + */ + setEllipse(ellipse: RectREllipseParams): void; +} + +//#endregion + +//#region WebGL2 + +export interface IGL2ProgramPrefix { + readonly VERTEX: string; + readonly FRAGMENT: string; +} + +export const enum UniformType { + Uniform1f, + Uniform1fv, + Uniform1i, + Uniform1iv, + Uniform1ui, + Uniform1uiv, + Uniform2f, + Uniform2fv, + Uniform2i, + Uniform2iv, + Uniform2ui, + Uniform2uiv, + Uniform3f, + Uniform3fv, + Uniform3i, + Uniform3iv, + Uniform3ui, + Uniform3uiv, + Uniform4f, + Uniform4fv, + Uniform4i, + Uniform4iv, + Uniform4ui, + Uniform4uiv +} + +export const enum UniformMatrix { + UMatrix2x2, + UMatrix2x3, + UMatrix2x4, + UMatrix3x2, + UMatrix3x3, + UMatrix3x4, + UMatrix4x2, + UMatrix4x3, + UMatrix4x4 +} + +export const enum AttribType { + Attrib1f, + Attrib1fv, + Attrib2f, + Attrib2fv, + Attrib3f, + Attrib3fv, + Attrib4f, + Attrib4fv, + AttribI4i, + AttribI4iv, + AttribI4ui, + AttribI4uiv +} + +export const enum RenderMode { + Arrays, + Elements, + ArraysInstanced, + ElementsInstanced +} + +export type ProgramConstructor = new ( + gl2: IWebGL2RenderItem, + vs?: string, + fs?: string +) => T; + +export interface IWebGL2RenderItem extends IRenderItem { + readonly UNIFORM_1f: UniformType.Uniform1f; + readonly UNIFORM_1fv: UniformType.Uniform1fv; + readonly UNIFORM_1i: UniformType.Uniform1i; + readonly UNIFORM_1iv: UniformType.Uniform1iv; + readonly UNIFORM_1ui: UniformType.Uniform1ui; + readonly UNIFORM_1uiv: UniformType.Uniform1uiv; + readonly UNIFORM_2f: UniformType.Uniform2f; + readonly UNIFORM_2fv: UniformType.Uniform2fv; + readonly UNIFORM_2i: UniformType.Uniform2i; + readonly UNIFORM_2iv: UniformType.Uniform2iv; + readonly UNIFORM_2ui: UniformType.Uniform2ui; + readonly UNIFORM_2uiv: UniformType.Uniform2uiv; + readonly UNIFORM_3f: UniformType.Uniform3f; + readonly UNIFORM_3fv: UniformType.Uniform3fv; + readonly UNIFORM_3i: UniformType.Uniform3i; + readonly UNIFORM_3iv: UniformType.Uniform3iv; + readonly UNIFORM_3ui: UniformType.Uniform3ui; + readonly UNIFORM_3uiv: UniformType.Uniform3uiv; + readonly UNIFORM_4f: UniformType.Uniform4f; + readonly UNIFORM_4fv: UniformType.Uniform4fv; + readonly UNIFORM_4i: UniformType.Uniform4i; + readonly UNIFORM_4iv: UniformType.Uniform4iv; + readonly UNIFORM_4ui: UniformType.Uniform4ui; + readonly UNIFORM_4uiv: UniformType.Uniform4uiv; + // uniform matrix 类型 + readonly U_MATRIX_2x2: UniformMatrix.UMatrix2x2; + readonly U_MATRIX_2x3: UniformMatrix.UMatrix2x3; + readonly U_MATRIX_2x4: UniformMatrix.UMatrix2x4; + readonly U_MATRIX_3x2: UniformMatrix.UMatrix3x2; + readonly U_MATRIX_3x3: UniformMatrix.UMatrix3x3; + readonly U_MATRIX_3x4: UniformMatrix.UMatrix3x4; + readonly U_MATRIX_4x2: UniformMatrix.UMatrix4x2; + readonly U_MATRIX_4x3: UniformMatrix.UMatrix4x3; + readonly U_MATRIX_4x4: UniformMatrix.UMatrix4x4; + // attribute 类型 + readonly ATTRIB_1f: AttribType.Attrib1f; + readonly ATTRIB_1fv: AttribType.Attrib1fv; + readonly ATTRIB_2f: AttribType.Attrib2f; + readonly ATTRIB_2fv: AttribType.Attrib2fv; + readonly ATTRIB_3f: AttribType.Attrib3f; + readonly ATTRIB_3fv: AttribType.Attrib3fv; + readonly ATTRIB_4f: AttribType.Attrib4f; + readonly ATTRIB_4fv: AttribType.Attrib4fv; + readonly ATTRIB_I4i: AttribType.AttribI4i; + readonly ATTRIB_I4iv: AttribType.AttribI4iv; + readonly ATTRIB_I4ui: AttribType.AttribI4ui; + readonly ATTRIB_I4uiv: AttribType.AttribI4uiv; + // 渲染模式 + readonly DRAW_ARRAYS: RenderMode.Arrays; + readonly DRAW_ELEMENTS: RenderMode.Elements; + readonly DRAW_ARRAYS_INSTANCED: RenderMode.ArraysInstanced; + readonly DRAW_ELEMENTS_INSTANCED: RenderMode.ElementsInstanced; + /** 最大纹理数量 */ + readonly MAX_TEXTURE_COUNT: number; + /** WebGL2 的画布 */ + readonly canvas: HTMLCanvasElement; + /** WebGL2 上下文 */ + readonly gl: WebGL2RenderingContext; + + /** + * 将画面渲染至帧缓冲 + * @param name 帧缓冲名称 + * @param texture 渲染至的纹理 + * @param clear 是否先清空画布再渲染 + */ + framebuffer(name: string, texture: IShaderTexture2D, clear?: boolean): void; + + /** + * 创建一个帧缓冲对象 + * @param name 帧缓冲名称 + * @returns 是否创建成功 + */ + createFramebuffer(name: string): boolean; + + /** + * 删除一个帧缓冲对象 + * @param name 帧缓冲名称 + * @returns 是否删除成功 + */ + deleteFramebuffer(name: string): boolean; + + /** + * 切换着色器程序 + * @param program 着色器程序 + */ + useProgram(program: IGL2Program): void; + + /** + * 创建一个着色器程序 + * @param vs 顶点着色器,可选 + * @param fs 片元着色器,可选 + */ + createProgram( + Program: ProgramConstructor, + vs?: string, + fs?: string + ): T; + + /** + * 删除一个着色器程序 + * @param program 要删除的着色器程序 + */ + deleteProgram(program: IGL2Program): void; +} + +type _U1 = [x0: number]; +type _U2 = [x0: number, x1: number]; +type _U3 = [x0: number, x1: number, x2: number]; +type _U4 = [x0: number, x1: number, x2: number, x3: number]; +type _UV = [data: T, srcOffset?: number, srcLength?: number]; +type _A = [data: T]; + +export interface UniformSetFn { + [UniformType.Uniform1f]: _U1; + [UniformType.Uniform1fv]: _UV; + [UniformType.Uniform1i]: _U1; + [UniformType.Uniform1iv]: _UV; + [UniformType.Uniform1ui]: _U1; + [UniformType.Uniform1uiv]: _UV; + [UniformType.Uniform2f]: _U2; + [UniformType.Uniform2fv]: _UV; + [UniformType.Uniform2i]: _U2; + [UniformType.Uniform2iv]: _UV; + [UniformType.Uniform2ui]: _U2; + [UniformType.Uniform2uiv]: _UV; + [UniformType.Uniform3f]: _U3; + [UniformType.Uniform3fv]: _UV; + [UniformType.Uniform3i]: _U3; + [UniformType.Uniform3iv]: _UV; + [UniformType.Uniform3ui]: _U3; + [UniformType.Uniform3uiv]: _UV; + [UniformType.Uniform4f]: _U4; + [UniformType.Uniform4fv]: _UV; + [UniformType.Uniform4i]: _U4; + [UniformType.Uniform4iv]: _UV; + [UniformType.Uniform4ui]: _U4; + [UniformType.Uniform4uiv]: _UV; +} + +export interface AttribSetFn { + [AttribType.Attrib1f]: _U1; + [AttribType.Attrib1fv]: _A; + [AttribType.Attrib2f]: _U2; + [AttribType.Attrib2fv]: _A; + [AttribType.Attrib3f]: _U3; + [AttribType.Attrib3fv]: _A; + [AttribType.Attrib4f]: _U4; + [AttribType.Attrib4fv]: _A; + [AttribType.AttribI4i]: _U4; + [AttribType.AttribI4iv]: _A; + [AttribType.AttribI4ui]: _U4; + [AttribType.AttribI4uiv]: _A; +} + +export interface IShaderUniform { + /** 这个 uniform 变量的内存位置 */ + readonly location: WebGLUniformLocation; + /** 这个 uniform 变量的类型 */ + readonly type: T; + /** 这个量所处的着色器程序 */ + readonly program: IGL2Program; + /** + * 设置这个 uniform 变量的值, + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/uniform + * @param params 要传递的参数,例如 uniform2f 就要传递 x0 x1 两个参数等,可以参考 mdn 文档 + */ + set(...params: UniformSetFn[T]): void; +} + +export interface IShaderAttrib { + /** 这个 attribute 常量的内存位置 */ + readonly location: number; + /** 这个 attribute 常量的类型 */ + readonly type: T; + /** 这个量所处的着色器程序 */ + readonly program: IGL2Program; + /** + * 设置这个 attribute 常量的值, + * 浮点数参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/vertexAttrib + * 整数参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribI + * @param params 要传递的参数 + */ + set(...params: AttribSetFn[T]): void; +} + +export interface IShaderAttribArray { + /** 这个 attribute 常量的内存位置 */ + readonly location: number; + /** 这个 attribute 所用的缓冲区信息 */ + readonly data: WebGLBuffer; + /** 这个量所处的着色器程序 */ + readonly program: IGL2Program; + /** + * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData + * @param data 数据 + * @param usage 用途 + */ + buffer(data: AllowSharedBufferSource | null, usage: GLenum): void; + /** + * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData + * @param data 数据 + * @param usage 用途 + * @param srcOffset 数据偏移量 + * @param length 数据长度 + */ + buffer( + data: ArrayBufferView, + usage: GLenum, + srcOffset: number, + length?: number + ): void; + /** + * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData + * @param dstByteOffset 数据修改的起始位置 + * @param srcData 数据 + */ + sub(dstByteOffset: GLintptr, srcData: AllowSharedBufferSource): void; + /** + * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData + * @param dstByteOffset 数据修改的起始位置 + * @param srcData 数据 + * @param srcOffset 数据偏移量 + * @param length 数据长度 + */ + sub( + dstByteOffset: GLintptr, + srcData: ArrayBufferView, + srcOffset: number, + length?: GLuint + ): void; + /** + * 告诉 gpu 将读取此 attribute 数据 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/vertexAttribPointer + * @param size 单个数据大小 + * @param type 数据类型 + * @param normalized 是否要经过归一化处理 + * @param stride 每一部分字节偏移量 + * @param offset 第一部分字节偏移量 + */ + pointer( + size: GLint, + type: GLenum, + normalized: GLboolean, + stride: GLsizei, + offset: GLintptr + ): void; + /** + * 告诉 gpu 将由整数类型读取此 attribute 数据 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribIPointer + * @param size 单个数据大小 + * @param type 数据类型 + * @param stride 每一部分字节偏移量 + * @param offset 第一部分字节偏移量 + */ + pointerI( + size: GLint, + type: GLenum, + stride: GLsizei, + offset: GLintptr + ): void; + /** + * 设置顶点指针更新时刻。 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/vertexAttribDivisor + * @param divisor 每多少个实例更新一次,0表示每个顶点都更新 + */ + divisor(divisor: number): void; + /** + * 启用这个顶点数据 + */ + enable(): void; + /** + * 禁用这个顶点数据 + */ + disable(): void; +} + +export interface IShaderIndices { + /** 这个顶点索引所用的缓冲区信息 */ + readonly data: WebGLBuffer; + /** 这个量所处的着色器程序 */ + readonly program: IGL2Program; + /** + * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData + * @param data 数据 + * @param usage 用途 + */ + buffer(data: AllowSharedBufferSource | null, usage: GLenum): void; + /** + * 修改缓冲区数据,会更改数据大小,重新分配内存,不更改数据大小的情况下建议使用 {@link sub} 代替。 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData + * @param data 数据 + * @param usage 用途 + * @param srcOffset 数据偏移量 + * @param length 数据长度 + */ + buffer( + data: ArrayBufferView, + usage: GLenum, + srcOffset: number, + length?: number + ): void; + /** + * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData + * @param dstByteOffset 数据修改的起始位置 + * @param srcData 数据 + */ + sub(dstByteOffset: GLintptr, srcData: AllowSharedBufferSource): void; + /** + * 修改缓冲区数据,但是不修改数据大小,不重新分配内存。 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferSubData + * @param dstByteOffset 数据修改的起始位置 + * @param srcData 数据 + * @param srcOffset 数据偏移量 + * @param length 数据长度 + */ + sub( + dstByteOffset: GLintptr, + srcData: ArrayBufferView, + srcOffset: number, + length?: GLuint + ): void; +} + +export interface IShaderUniformMatrix { + /** 矩阵的内存位置 */ + readonly location: WebGLUniformLocation; + /** 矩阵类型 */ + readonly type: UniformMatrix; + /** 这个量所处的着色器程序 */ + readonly program: IGL2Program; + /** + * 设置矩阵的值,参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/uniformMatrix + * @param transpose 是否转置矩阵 + * @param data 矩阵数据,列主序 + * @param srcOffset 数据偏移量 + * @param srcLength 数据长度 + */ + set( + transpose: GLboolean, + data: Float32List, + srcOffset?: number, + srcLength?: number + ): void; +} + +export interface IShaderUniformBlock { + /** 这个 uniform block 的内存地址 */ + readonly location: GLuint; + /** 与这个 uniform block 所绑定的缓冲区 */ + readonly buffer: WebGLBuffer; + /** 这个 uniform block 的大小 */ + readonly size: number; + /** 这个量所处的着色器程序 */ + readonly program: IGL2Program; + /** + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/bindBufferBase + * @param srcData 要设置为的值 + */ + set(srcData: AllowSharedBufferSource | null): void; + /** + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL2RenderingContext/bindBufferBase + * @param srcData 要设置为的值 + * @param srcOffset 数据偏移量 + * @param length 数据长度 + */ + set(srcData: ArrayBufferView, srcOffset: number, length?: number): void; +} + +export interface IShaderTexture2D { + /** 纹理对象 */ + readonly texture: WebGLTexture; + /** 宽度 */ + readonly width: number; + /** 高度 */ + readonly height: number; + /** 纹理所属索引 */ + readonly index: number; + /** 这个量所处的着色器程序 */ + readonly program: IGL2Program; + /** + * 设置这个纹理的图像,不建议使用,会修改宽高 + * @param source 要设置成的图像源 + */ + set(source: TexImageSource): void; + /** + * 设置纹理的一部分信息,不会修改宽高 + * @param source 要设置的图像源 + * @param x 要设置到的起始点横坐标 + * @param y 要设置到的起始点纵坐标 + * @param width 宽度 + * @param height 高度 + */ + sub( + source: TexImageSource, + x: number, + y: number, + width: number, + height: number + ): void; +} + +export interface DrawArraysParam { + mode: GLenum; + first: number; + count: number; +} + +export interface DrawElementsParam { + mode: GLenum; + count: number; + type: GLenum; + offset: GLintptr; +} + +export interface DrawArraysInstancedParam { + mode: GLenum; + first: number; + count: number; + instanceCount: number; +} + +export interface DrawElementsInstancedParam { + mode: GLenum; + count: number; + type: GLenum; + offset: GLintptr; + instanceCount: number; +} + +export interface DrawParamsMap { + [RenderMode.Arrays]: DrawArraysParam; + [RenderMode.ArraysInstanced]: DrawArraysInstancedParam; + [RenderMode.Elements]: DrawElementsParam; + [RenderMode.ElementsInstanced]: DrawElementsInstancedParam; +} + +export interface IGL2Program { + /** webgl2上下文 */ + readonly gl: WebGL2RenderingContext; + /** 当前着色器程序的着色器渲染元素 */ + readonly element: IWebGL2RenderItem; + /** 当前的webgl程序 */ + readonly program: WebGLProgram | null; + /** 当前正在使用的顶点索引数组 */ + readonly usingIndices: IShaderIndices | null; + /** 渲染模式 */ + readonly renderMode: RenderMode; + + /** + * 渲染前准备 + */ + ready(): boolean; + + /** + * 设置渲染模式,目前可选 {@link IWebGL2RenderItem.DRAW_ARRAYS} 至 {@link IWebGL2RenderItem.DRAW_INSTANCED} + */ + mode(mode: RenderMode): void; + + /** + * 获取指定渲染模式的渲染参数 + * @param param 渲染模式 + */ + getDrawParams( + param: T + ): Readonly | null; + + /** + * 设置 DRAW_ARRAYS 模式下的渲染参数 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/drawArrays + * @param mode 渲染模式 + * @param first 第一个元素的位置 + * @param count 渲染多少个元素 + */ + paramArrays(mode: GLenum, first: number, count: number): void; + + /** + * 设置 DRAW_ARRAYS_INSTANCED 模式下的渲染参数 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/drawArraysInstanced + * @param mode 渲染模式 + * @param first 第一个元素的位置 + * @param count 渲染多少个元素 + * @param instanceCount 渲染实例数量 + */ + paramArraysInstanced( + mode: GLenum, + first: number, + count: number, + instanceCount: number + ): void; + + /** + * 设置 DRAW_ELEMENTS 模式下的渲染参数 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/drawElements + * @param mode 渲染模式 + * @param count 渲染元素数量 + * @param type 数据类型 + * @param offset 偏移量 + */ + paramElements( + mode: GLenum, + count: number, + type: GLenum, + offset: number + ): void; + + /** + * 设置 DRAW_ELEMENTS 模式下的渲染参数 + * 参考 https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/drawElementsInstanced + * @param mode 渲染模式 + * @param count 渲染元素数量 + * @param type 数据类型 + * @param offset 偏移量 + * @param instanceCount 渲染实例数量 + */ + paramElementsInstanced( + mode: GLenum, + count: number, + type: GLenum, + offset: number, + instanceCount: number + ): void; + + /** + * 切换渲染时使用的顶点索引 + * @param name 要使用的顶点索引名称 + */ + useIndices(name: string | IShaderIndices): void; + + /** + * 检查当前是否需要重新编译着色器,如果需要,则重新编译 + * @param force 是否强制重新编译 + * @returns 是否执行了编译操作 + */ + requestCompile(force?: boolean): boolean; + + /** + * 设置顶点着色器内容 + * @param vs 顶点着色器 + */ + vs(vs: string): void; + + /** + * 设置片元着色器内容 + * @param fs 片元着色器 + */ + fs(fs: string): void; + + /** + * 当这个程序被卸载时执行的函数 + */ + unload(): void; + + /** + * 当这个程序被加载(使用)时执行的函数 + */ + load(): void; + + /** + * 获取一个uniform,需要事先定义,否则返回null + * @param uniform uniform名称 + */ + getUniform( + uniform: string + ): IShaderUniform | null; + + /** + * 获取一个attribute,需要事先定义,否则返回null + * @param attrib attribute名称 + */ + getAttribute( + attrib: string + ): IShaderAttrib | null; + + /** + * 获取一个attribute array,需要事先定义,否则返回null + * @param name attribute array名称 + */ + getAttribArray(name: string): IShaderAttribArray | null; + + /** + * 获取一个顶点索引数组,需要提前定义,否则返回null + * @param name 顶点索引数组的名称 + */ + getIndices(name: string): IShaderIndices | null; + + /** + * 获取一个 uniform matrix,需要事先定义,否则返回null + * @param matrix uniform matrix 的名称 + */ + getMatrix(matrix: string): IShaderUniformMatrix | null; + + /** + * 获取一个 uniform block,例如 UBO,需要事先定义,否则返回null + * @param block uniform block 的名称 + */ + getUniformBlock(block: string): IShaderUniformBlock | null; + + /** + * 获取一个 texture,需要事先定义,否则返回null + * @param name texture 的名称 + */ + getTexture(name: string): IShaderTexture2D | null; + + /** + * 定义一个 uniform 变量,并存入本着色器程序的 uniform 变量映射 + * @param uniform uniform 变量名 + * @param type uniform 类型,可选 {@link IWebGL2RenderItem.UNIFORM_1f} 至 {@link IWebGL2RenderItem.UNIFORM_4uiv} + * @returns uniform 变量的操作对象,可用于设置其值 + */ + defineUniform( + uniform: string, + type: T + ): IShaderUniform | null; + + /** + * 定义一个 uniform 矩阵变量,并存入本着色器程序的 uniform 矩阵变量映射 + * @param uniform uniform 矩阵变量名 + * @param type uniform 矩阵类型,可选 {@link IWebGL2RenderItem.U_MATRIX_2x2} 至 {@link IWebGL2RenderItem.U_MATRIX_4x4} + * @returns uniform 矩阵变量的操作对象,可用于设置其值 + */ + defineUniformMatrix( + uniform: string, + type: UniformMatrix + ): IShaderUniformMatrix | null; + + /** + * 定义一个 attribute 常量,并存入本着色器程序的 attribute 常量映射,在 es 300 版本中叫做 in + * @param attrib attribute 常量名 + * @param type attribute 类型,可选 {@link IWebGL2RenderItem.ATTRIB_1f} 至 {@link IWebGL2RenderItem.ATTRIB_I4uiv} + * @returns attribute 常量的操作对象,可用于设置其值 + */ + defineAttribute( + attrib: string, + type: T + ): IShaderAttrib | null; + + /** + * 定义一个顶点数组 + * @param name 顶点数组名称 + */ + defineAttribArray(name: string): IShaderAttribArray | null; + + /** + * 定义一个顶点索引数组 + * @param name 顶点索引数组的名称 + */ + defineIndices(name: string): IShaderIndices | null; + + /** + * 定义一个 uniform block,例如 UBO,并存入本着色器程序的 uniform block 映射 + * 用于一次性向着色器传输大量数据 + * @param block uniform block 名称 + * @param size 数据量,即数据长度,例如一个vec4就是4个长度 + * @param usage 缓冲区用途,例如 gl.STATIC_DRAW 是指会频繁读取但不会频繁写入 + * 参考 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/bufferData + * 的 `usage` 参数 + * @param binding uniform block 的索引,例如这是你设置的第一个uniform block,就可以填0,第二个填1,以此类推 + * @returns uniform block 的操作对象,可用于设置其值 + */ + defineUniformBlock( + block: string, + size: number, + usage: number, + binding: number + ): IShaderUniformBlock | null; + + /** + * 定义一个材质 + * @param name 纹理名称 + * @param index 纹理索引,根据不同浏览器,其最大数量不一定相等,根据标准其数量应该大于等于 8 个, + * 因此考虑到兼容性,不建议纹理数量超过 8 个。 + * @param w 纹理的宽度 + * @param h 纹理的高度 + * @returns 这个 texture 的操作对象,可以用于设置其内容 + */ + defineTexture( + name: string, + index: number, + w?: number, + h?: number + ): IShaderTexture2D | null; + + /** + * 绑定纹理,自动判断应该使用 sub 还是 set + * @param program 使用的着色器程序 + * @param texture 要绑定至的纹理 + * @param source 纹理内容 + * @returns 是否绑定成功 + */ + texTexture(texture: string, source: SizedCanvasImageSource): boolean; + + /** + * 摧毁这个着色器程序,不要直接调用,请使用 {@link IWebGL2RenderItem.deleteProgram} 来删除一个着色器程序 + */ + destroy(): void; +} + +//#endregion diff --git a/packages/render/src/core/utils.ts b/packages/render/src/core/utils.ts index f08cc04..db44551 100644 --- a/packages/render/src/core/utils.ts +++ b/packages/render/src/core/utils.ts @@ -1,34 +1,6 @@ -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 - | DefineComponent -> = T extends keyof JSX.IntrinsicElements - ? JSX.IntrinsicElements[T] - : T extends DefineSetupFnComponent - ? InstanceType['$props'] & InstanceType['$emits'] - : T extends DefineComponent - ? InstanceType['$props'] & InstanceType['$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() { @@ -47,28 +19,6 @@ 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(set1: Set, set2: Set) { - if (set1 === set2) return true; - else return set1.size === set2.size && set1.isSubsetOf(set2); -} - export function transformCanvas( canvas: MotaOffscreenCanvas2D, transform: Transform diff --git a/packages/render/src/elements/index.ts b/packages/render/src/elements/index.ts deleted file mode 100644 index 4bdb3b2..0000000 --- a/packages/render/src/elements/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './graphics'; -export * from './misc'; diff --git a/packages/render/src/index.ts b/packages/render/src/index.ts index 29387d7..eea9e38 100644 --- a/packages/render/src/index.ts +++ b/packages/render/src/index.ts @@ -1,5 +1,4 @@ export * from './assets'; export * from './core'; -export * from './elements'; export * from './style'; export * from './types'; diff --git a/public/libs/ui.js b/public/libs/ui.js index ab1fb3d..1e26d18 100644 --- a/public/libs/ui.js +++ b/public/libs/ui.js @@ -2375,7 +2375,7 @@ ui.prototype._drawQuickShop = function () { }; }); choices.push('返回游戏'); - this.drawChoices(null, choices, void 0, true); + this.drawChoices2(null, choices, void 0, true); }; ui.prototype._drawSyncSave = function () { diff --git a/tsconfig.json b/tsconfig.json index 6b4a3e2..ea6ff69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,10 +15,9 @@ "lib": ["ESNext", "DOM", "DOM.Iterable"], "skipLibCheck": true, "noEmit": true, - "baseUrl": ".", "paths": { - "@motajs/*": ["packages/*/src"], - "@user/*": ["packages-user/*/src"] + "@motajs/*": ["./packages/*/src"], + "@user/*": ["./packages-user/*/src"] } }, "include": [ diff --git a/tsconfig.node.json b/tsconfig.node.json index 756a862..83c6fa6 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -8,12 +8,12 @@ "strict": true }, "include": [ - "vite.config.ts", - "script/**/*.ts", - "docs/**/*.ts", - "docs/.vitepress/api.ts", - "docs/.vitepress/config.ts", - "docs/.vitepress/apiSidebar.ts", - "docs/.vitepress/init.ts" + "./vite.config.ts", + "./script/**/*.ts", + "./docs/**/*.ts", + "./docs/.vitepress/api.ts", + "./docs/.vitepress/config.ts", + "./docs/.vitepress/apiSidebar.ts", + "./docs/.vitepress/init.ts" ] } diff --git a/vite.config.ts b/vite.config.ts index 85d5e89..3b2ec72 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,9 +6,8 @@ import postcssPresetEnv from 'postcss-preset-env'; import * as glob from 'glob'; const custom = [ - 'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom', - 'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin', - 'container-custom', 'map-render' + 'container', 'custom', 'text', 'image', 'shader', 'comment', 'custom-container', + 'map-render', 'animate', 'damage', 'graphics', 'icon', 'winskin', ]; const aliases = glob.sync('packages/*/src').map((srcPath) => {