From bca679d4b1eb053b61caa596d2f1c2f111d9a776 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 25 Feb 2025 21:33:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E7=AD=96=E7=95=A5=20&?= =?UTF-8?q?=20=E6=96=87=E6=9C=AC=E6=A1=86=E6=98=BE=E7=A4=BA=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/libs/actions.js | 2 +- public/libs/events.js | 8 +- src/core/fx/webgl.ts | 12 +- src/core/render/container.ts | 5 +- src/core/render/item.ts | 30 +- src/core/render/preset/misc.ts | 12 +- src/module/render/components/textbox.tsx | 394 +++++++++++-------- src/module/render/components/textboxTyper.ts | 2 + src/module/render/index.tsx | 5 + src/module/render/ui/main.tsx | 6 +- 10 files changed, 290 insertions(+), 186 deletions(-) diff --git a/public/libs/actions.js b/public/libs/actions.js index 422db39..d95cace 100644 --- a/public/libs/actions.js +++ b/public/libs/actions.js @@ -1117,7 +1117,7 @@ actions.prototype._clickAction_text = function () { // 正在淡入淡出的话不执行 if (core.status.event.animateUI) return; - const Store = Mota.require('module', 'Render').TextboxStore; + const Store = Mota.require('module', 'MainUI').TextboxStore; const store = Store.get('main-textbox'); // var data = core.clone(core.status.event.data.current); diff --git a/public/libs/events.js b/public/libs/events.js index b88a27f..a4299c5 100644 --- a/public/libs/events.js +++ b/public/libs/events.js @@ -1553,7 +1553,7 @@ events.prototype.__action_doAsyncFunc = function (isAsync, func) { events.prototype._action_text = function (data, x, y, prefix) { if (this.__action_checkReplaying()) return; - const Store = Mota.require('module', 'Render').TextboxStore; + const Store = Mota.require('module', 'MainUI').TextboxStore; const store = Store.get('main-textbox'); const { text } = data; let title = ''; @@ -1594,10 +1594,10 @@ events.prototype._action_text = function (data, x, y, prefix) { } } - const showTitle = - text.slice(0, titleStartIndex) + text.slice(titleEndIndex); + const showText = text.slice(0, titleStartIndex) + text.slice(titleEndIndex); store.show(); - store.modify({ text: showTitle, title }); + store.modify({ title }); + store.setText(showText); // data.text = core.replaceText(data.text, prefix); // var ctx = data.code ? '__text__' + data.code : null; diff --git a/src/core/fx/webgl.ts b/src/core/fx/webgl.ts index 770d8cb..1666878 100644 --- a/src/core/fx/webgl.ts +++ b/src/core/fx/webgl.ts @@ -13,7 +13,7 @@ function checkSupport() { sleep(3000).then(() => { tip( 'warning', - `您的浏览器不支持WebGL,大部分特效将会无法显示,建议使用新版浏览器` + `您的浏览器不支持WebGL,大部分效果将会无法显示,请更新你的浏览器` ); }); } @@ -21,7 +21,7 @@ function checkSupport() { sleep(3000).then(() => { tip( 'warning', - `您的浏览器不支持WebGL2,一部分特效将会无法显示,建议使用新版浏览器` + `您的浏览器不支持WebGL2,大部分效果将会无法显示,请更新你的浏览器` ); }); } @@ -54,10 +54,10 @@ type UniformFunc< type UniformBinderValue = N extends 1 ? number : N extends 2 - ? [number, number] - : N extends 3 - ? [number, number, number] - : [number, number, number, number]; + ? [number, number] + : N extends 3 + ? [number, number, number] + : [number, number, number, number]; interface UniformBinder< N extends UniformBinderNum, diff --git a/src/core/render/container.ts b/src/core/render/container.ts index 8eedd80..77a282b 100644 --- a/src/core/render/container.ts +++ b/src/core/render/container.ts @@ -80,9 +80,9 @@ export class Container append(parent: RenderItem): void { super.append(parent); if (this.root) { + const root = this.root; this.forEachChild(ele => { - ele.checkRoot(); - this.root?.connect(ele); + ele.setRoot(root); }); } } @@ -104,6 +104,7 @@ export class Container this.sortedChildren = [...this.children].sort( (a, b) => a.zIndex - b.zIndex ); + this.update(); } protected propagateEvent( diff --git a/src/core/render/item.ts b/src/core/render/item.ts index 121ccb0..fdd345b 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -437,6 +437,15 @@ export abstract class RenderItem return canvas; } + /** + * 删除由 `requireCanvas` 申请的画布,当画布不再使用时,可以用该方法删除画布 + * @param canvas 要删除的画布 + */ + protected deleteCanvas(canvas: MotaOffscreenCanvas2D) { + if (!this.canvases.delete(canvas)) return; + canvas.delete(); + } + //#region 修改元素属性 /** @@ -457,8 +466,8 @@ export abstract class RenderItem * @param y 纵坐标 */ pos(x: number, y: number) { + // 这个函数会调用 update,因此不再手动调用 update this._transform.setTranslate(x, y); - this.update(); } /** @@ -588,10 +597,15 @@ export abstract class RenderItem } update(item: RenderItem = this): void { - if (this.cacheDirty) return; - this.cacheDirty = true; - if (this.hidden) return; - this.parent?.update(item); + 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() { @@ -650,6 +664,12 @@ export abstract class RenderItem //#region 父子关系 + setRoot(item: RenderItem & IRenderTreeRoot) { + this._root?.disconnect(this); + this._root = item; + item.connect(item); + } + checkRoot(): RenderItem | null { if (this._root) return this._root; if (this.isRoot) return this; diff --git a/src/core/render/preset/misc.ts b/src/core/render/preset/misc.ts index ca5f8f6..94bdeeb 100644 --- a/src/core/render/preset/misc.ts +++ b/src/core/render/preset/misc.ts @@ -30,7 +30,10 @@ export class Text extends RenderItem { super(type, false); this.text = text; - if (text.length > 0) this.calBox(); + if (text.length > 0) { + this.calBox(); + this.emit('setText', text); + } } protected render( @@ -70,7 +73,7 @@ export class Text extends RenderItem { setText(text: string) { this.text = text; this.calBox(); - if (this.parent) this.update(this); + this.update(this); this.emit('setText', text); } @@ -81,7 +84,7 @@ export class Text extends RenderItem { setFont(font: string) { this.font = font; this.calBox(); - if (this.parent) this.update(this); + this.update(this); } /** @@ -410,8 +413,7 @@ export class Winskin extends RenderItem { Winskin.patternMap.set(this.imageName, winskinPattern); } this.patternCache = winskinPattern; - pattern.delete(); - this.canvases.delete(pattern); + this.deleteCanvas(pattern); return winskinPattern; } diff --git a/src/module/render/components/textbox.tsx b/src/module/render/components/textbox.tsx index 732a546..9ad0888 100644 --- a/src/module/render/components/textbox.tsx +++ b/src/module/render/components/textbox.tsx @@ -2,8 +2,8 @@ import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { computed, defineComponent, + nextTick, onUnmounted, - onUpdated, ref, shallowReactive, shallowRef, @@ -13,7 +13,7 @@ import { } from 'vue'; import { logger } from '@/core/common/logger'; import { Sprite } from '@/core/render/sprite'; -import { ContainerProps, DefaultProps } from '@/core/render/renderer'; +import { DefaultProps } from '@/core/render/renderer'; import { isNil } from 'lodash-es'; import { SetupComponentOptions } from './types'; import EventEmitter from 'eventemitter3'; @@ -22,7 +22,9 @@ import { ITextContentConfig, TextContentTyper, TyperRenderable, - TextContentType + TextContentType, + WordBreak, + TextAlign } from './textboxTyper'; export interface TextContentProps @@ -34,8 +36,6 @@ export interface TextContentProps fill?: boolean; /** 是否描边 */ stroke?: boolean; - /** 是否忽略打字机,直接显伤全部 */ - showAll?: boolean; } export type TextContentEmits = { @@ -43,6 +43,18 @@ export type TextContentEmits = { typeStart: () => void; }; +export interface TextContentExpose { + /** + * 重新开始打字 + */ + retype(): void; + + /** + * 立刻显示所有文字 + */ + showAll(): void; +} + const textContentOptions = { props: [ 'breakChars', @@ -50,7 +62,6 @@ const textContentOptions = { 'fontSize', 'fontWeight', 'fontItalic', - 'height', 'ignoreLineEnd', 'ignoreLineStart', 'interval', @@ -58,17 +69,14 @@ const textContentOptions = { 'lineHeight', 'text', 'textAlign', - 'width', 'wordBreak', - 'x', - 'y', 'fill', 'fillStyle', 'strokeStyle', 'strokeWidth', 'stroke', - 'showAll', - 'loc' + 'loc', + 'width' ], emits: ['typeEnd', 'typeStart'] } satisfies SetupComponentOptions< @@ -81,45 +89,42 @@ export const TextContent = defineComponent< TextContentProps, TextContentEmits, keyof TextContentEmits ->((props, { emit }) => { - if (props.width && props.width <= 0) { +>((props, { emit, expose }) => { + const width = computed(() => props.width ?? props.loc?.[2] ?? 200); + if (width.value < 0) { logger.warn(41, String(props.width)); } const typer = new TextContentTyper(props); let renderable: TyperRenderable[] = []; let needUpdate = false; - let nowText = ''; - - watch(props, value => { - typer.setConfig(value); - }); const retype = () => { - if (props.showAll) { - typer.typeAll(); - } - if (props.text === nowText) return; if (needUpdate) return; needUpdate = true; if (!spriteElement.value) { needUpdate = false; } - nowText = props.text ?? ''; renderable = []; spriteElement.value?.requestBeforeFrame(() => { typer.setConfig(props); typer.setText(props.text ?? ''); typer.type(); - if (props.showAll) { - typer.typeAll(); - } needUpdate = false; }); }; - onUpdated(retype); + const showAll = () => { + typer.typeAll(); + }; + + watch(props, value => { + typer.setConfig(value); + retype(); + }); + + expose({ retype, showAll }); const spriteElement = shallowRef(); const renderContent = (canvas: MotaOffscreenCanvas2D) => { @@ -164,19 +169,15 @@ export const TextContent = defineComponent< return () => { return ( ); }; }, textContentOptions); -export interface TextboxProps extends TextContentProps, ContainerProps { +export interface TextboxProps extends TextContentProps, DefaultProps { /** 背景颜色 */ backColor?: CanvasStyle; /** 背景 winskin */ @@ -195,6 +196,28 @@ export interface TextboxProps extends TextContentProps, ContainerProps { titlePadding?: number; } +export interface TextboxExpose { + /** + * 显示这个文本框 + */ + show(): void; + + /** + * 隐藏这个文本框 + */ + hide(): void; + + /** + * 重新开始打字 + */ + retype(): void; + + /** + * 立刻显示所有文字 + */ + showAll(): void; +} + type TextboxEmits = TextContentEmits; type TextboxSlots = SlotsType<{ default: (data: TextboxProps) => VNode[]; @@ -205,23 +228,14 @@ const textboxOptions = { props: (textContentOptions.props as (keyof TextboxProps)[]).concat([ 'backColor', 'winskin', - 'id', 'padding', - 'alpha', - 'hidden', - 'anchorX', - 'anchorY', - 'anti', - 'cache', - 'composite', - 'fall', - 'hd', - 'transform', - 'type', - 'zIndex', 'titleFill', 'titleStroke', - 'titleFont' + 'titleFont', + 'titlePadding', + 'id', + 'hidden', + 'title' ]), emits: textContentOptions.emits } satisfies SetupComponentOptions; @@ -236,153 +250,206 @@ export const Textbox = defineComponent< TextboxEmits, keyof TextboxEmits, TextboxSlots ->((props, { slots }) => { - const data = shallowReactive({ ...props }); - data.padding ??= 8; - data.width ??= 200; - data.height ??= 200; - data.id ??= ''; - data.alpha ??= 1; - data.titleFill ??= '#000'; - data.titleStroke ??= 'transparent'; - data.titleFont ??= '16px Verdana'; - data.titlePadding ??= 4; +>((props, { slots, expose }) => { + const contentData = shallowReactive({}); + const data = shallowReactive({}); + + const setContentData = () => { + contentData.breakChars = props.breakChars ?? ''; + contentData.fontFamily = props.fontFamily ?? 'Verdana'; + contentData.fontSize = props.fontSize ?? 16; + contentData.fontWeight = props.fontWeight ?? 500; + contentData.fontItalic = props.fontItalic ?? false; + contentData.ignoreLineEnd = props.ignoreLineEnd ?? ''; + contentData.ignoreLineStart = props.ignoreLineStart ?? ''; + contentData.interval = props.interval ?? 0; + contentData.keepLast = props.keepLast ?? false; + contentData.lineHeight = props.lineHeight ?? 0; + contentData.text = props.text ?? ''; + contentData.textAlign = props.textAlign ?? TextAlign.Left; + contentData.wordBreak = props.wordBreak ?? WordBreak.Space; + contentData.fill = props.fill ?? true; + contentData.stroke = props.stroke ?? false; + contentData.fillStyle = props.fillStyle ?? '#fff'; + contentData.strokeStyle = props.strokeStyle ?? '#000'; + contentData.strokeWidth = props.strokeWidth ?? 2; + contentData.loc = props.loc; + contentData.width = props.width; + }; + + const setTextboxData = () => { + data.backColor = props.backColor ?? '#222'; + data.winskin = props.winskin; + data.padding = props.padding ?? 8; + data.titleFill = props.titleFill ?? 'gold'; + data.titleStroke = props.titleStroke ?? 'transparent'; + data.titleFont = props.titleFont ?? '18px Verdana'; + data.titlePadding = props.titlePadding ?? 8; + data.width = props.width ?? props.loc?.[2] ?? 200; + data.height = props.height ?? props.loc?.[3] ?? 200; + data.title = props.title ?? ''; + }; + + setContentData(); + setTextboxData(); + + watch(props, () => { + const needUpdateTitle = data.title !== props.title; + setTextboxData(); + if (needUpdateTitle) { + onSetText(); + } + }); const titleElement = ref(); - const titleWidth = ref(data.titlePadding * 2); - const titleHeight = ref(data.titlePadding * 2); + const content = ref(); + const hidden = ref(props.hidden); + /** 标题宽度 */ + const tw = ref(data.titlePadding! * 2); + /** 标题高度 */ + const th = ref(data.titlePadding! * 2); const contentY = computed(() => { - const height = titleHeight.value; + const height = th.value; return data.title ? height : 0; }); + const backHeight = computed(() => data.height! - contentY.value); const contentWidth = computed(() => data.width! - data.padding! * 2); const contentHeight = computed( () => data.height! - data.padding! * 2 - contentY.value ); - const calTitleSize = (text: string) => { - if (!titleElement.value) return; - const { width, height } = titleElement.value; - titleWidth.value = width + data.titlePadding! * 2; - titleHeight.value = height + data.titlePadding! * 2; - data.title = text; + const onSetText = () => { + nextTick(() => { + titleElement.value?.requestBeforeFrame(() => { + if (titleElement.value) { + const { width, height } = titleElement.value; + tw.value = width + data.padding! * 2; + th.value = height + data.padding! * 2; + } + }); + }); }; - watch(titleElement, (value, old) => { - old?.off('setText', calTitleSize); - value?.on('setText', calTitleSize); - if (value) calTitleSize(value?.text); - }); - - onUnmounted(() => { - titleElement.value?.off('setText', calTitleSize); - }); - // ----- store /** 结束打字机 */ const storeEmits: TextboxStoreEmits = { endType() { - data.showAll = true; + content.value?.showAll(); + }, + hide() { + hidden.value = true; + }, + show() { + hidden.value = false; + }, + update(value) { + if (data.title !== value.title) { + data.title = value.title; + onSetText(); + } + }, + setText(text) { + if (contentData.text === text) { + content.value?.retype(); + } else { + contentData.text = text; + } } }; const store = TextboxStore.use( props.id ?? getNextTextboxId(), - data, + contentData, storeEmits ); - const hidden = ref(data.hidden); - store.on('hide', () => (hidden.value = true)); - store.on('show', () => (hidden.value = false)); - store.on('update', value => { - if (value.title) { - titleElement.value?.requestBeforeFrame(() => { - const { width, height } = titleElement.value!; - titleWidth.value = width + data.padding! * 2; - titleHeight.value = height + data.padding! * 2; - }); - } - }); const onTypeStart = () => { store.emitTypeStart(); }; const onTypeEnd = () => { - data.showAll = false; store.emitTypeEnd(); }; - return () => { - return ( - - ); - }; + expose({ + show() { + hidden.value = false; + }, + hide() { + hidden.value = true; + }, + retype() { + content.value?.retype(); + }, + showAll() { + content.value?.showAll(); + } + }); + + return () => ( + + ); }, textboxOptions); interface TextboxStoreEmits { endType: () => void; + hide: () => void; + show: () => void; + update: (value: TextboxProps) => void; + setText: (text: string) => void; } interface TextboxStoreEvent { @@ -436,21 +503,30 @@ export class TextboxStore extends EventEmitter { // @ts-expect-error 无法推导 if (!isNil(value)) this.data[key] = value; } + this.emits.update(this.data); this.emit('update', this.data); } + /** + * 设置显示的文本 + * @param text 要显示的文本 + */ + setText(text: string) { + this.emits.setText(text); + } + /** * 显示文本框 */ show() { - this.emit('show'); + this.emits.show(); } /** * 隐藏文本框 */ hide() { - this.emit('hide'); + this.emits.hide(); } /** diff --git a/src/module/render/components/textboxTyper.ts b/src/module/render/components/textboxTyper.ts index 0b7fa76..e872339 100644 --- a/src/module/render/components/textboxTyper.ts +++ b/src/module/render/components/textboxTyper.ts @@ -422,6 +422,7 @@ export class TextContentTyper extends EventEmitter { * 开始打字 */ type() { + if (this.typing) return; if (this.config.interval === 0) { this.emit('typeStart'); this.typeChars(Infinity); @@ -438,6 +439,7 @@ export class TextContentTyper extends EventEmitter { * 立即显示所有文字 */ typeAll() { + if (!this.typing) return; this.typeChars(Infinity); this.render?.(this.renderData, false); } diff --git a/src/module/render/index.tsx b/src/module/render/index.tsx index 0bfa2d6..b11ff97 100644 --- a/src/module/render/index.tsx +++ b/src/module/render/index.tsx @@ -4,6 +4,7 @@ import { defineComponent } from 'vue'; import { UIController } from '@/core/system'; import { mainSceneUI } from './ui/main'; import { MAIN_HEIGHT, MAIN_WIDTH } from './shared'; +import { TextboxStore } from './components'; export function create() { const main = new MotaRenderer(); @@ -34,6 +35,10 @@ export function create() { console.log(main); } +Mota.register('module', 'MainUI', { + TextboxStore +}); + export * from './components'; export * from './ui'; export * from './use'; diff --git a/src/module/render/ui/main.tsx b/src/module/render/ui/main.tsx index 7e467d5..9a71dd9 100644 --- a/src/module/render/ui/main.tsx +++ b/src/module/render/ui/main.tsx @@ -57,9 +57,7 @@ const MainScene = defineComponent(() => { const mainTextboxProps: Props = { text: '', hidden: true, - width: 480, - height: 150, - y: 330, + loc: [0, 330, 480, 150], zIndex: 30, fillStyle: '#fff', titleFill: 'gold', @@ -67,7 +65,7 @@ const MainScene = defineComponent(() => { titleFont: '700 20px normal', winskin: 'winskin2.png', interval: 100, - lineHeight: 6 + lineHeight: 4 }; const map = ref();