Compare commits

...

4 Commits

Author SHA1 Message Date
d0dae40a5a fix: TextContent 的各种标签 2025-02-25 23:06:50 +08:00
72f02726ba fix: TextContent 的行高问题 2025-02-25 21:42:16 +08:00
bca679d4b1 fix: 更新策略 & 文本框显示问题 2025-02-25 21:33:10 +08:00
df993a7242 refactor: winskin 绘制 2025-02-25 18:01:34 +08:00
11 changed files with 598 additions and 470 deletions

View File

@ -1117,7 +1117,7 @@ actions.prototype._clickAction_text = function () {
// 正在淡入淡出的话不执行 // 正在淡入淡出的话不执行
if (core.status.event.animateUI) return; 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'); const store = Store.get('main-textbox');
// var data = core.clone(core.status.event.data.current); // var data = core.clone(core.status.event.data.current);

View File

@ -1553,7 +1553,7 @@ events.prototype.__action_doAsyncFunc = function (isAsync, func) {
events.prototype._action_text = function (data, x, y, prefix) { events.prototype._action_text = function (data, x, y, prefix) {
if (this.__action_checkReplaying()) return; 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 store = Store.get('main-textbox');
const { text } = data; const { text } = data;
let title = ''; let title = '';
@ -1594,10 +1594,10 @@ events.prototype._action_text = function (data, x, y, prefix) {
} }
} }
const showTitle = const showText = text.slice(0, titleStartIndex) + text.slice(titleEndIndex);
text.slice(0, titleStartIndex) + text.slice(titleEndIndex);
store.show(); store.show();
store.modify({ text: showTitle, title }); store.modify({ title });
store.setText(showText);
// data.text = core.replaceText(data.text, prefix); // data.text = core.replaceText(data.text, prefix);
// var ctx = data.code ? '__text__' + data.code : null; // var ctx = data.code ? '__text__' + data.code : null;

View File

@ -13,7 +13,7 @@ function checkSupport() {
sleep(3000).then(() => { sleep(3000).then(() => {
tip( tip(
'warning', 'warning',
`您的浏览器不支持WebGL大部分特效将会无法显示,建议使用新版浏览器` `您的浏览器不支持WebGL大部分效果将会无法显示,请更新你的浏览器`
); );
}); });
} }
@ -21,7 +21,7 @@ function checkSupport() {
sleep(3000).then(() => { sleep(3000).then(() => {
tip( tip(
'warning', 'warning',
`您的浏览器不支持WebGL2一部分特效将会无法显示,建议使用新版浏览器` `您的浏览器不支持WebGL2大部分效果将会无法显示,请更新你的浏览器`
); );
}); });
} }

View File

@ -80,9 +80,9 @@ export class Container<E extends EContainerEvent = EContainerEvent>
append(parent: RenderItem): void { append(parent: RenderItem): void {
super.append(parent); super.append(parent);
if (this.root) { if (this.root) {
const root = this.root;
this.forEachChild(ele => { this.forEachChild(ele => {
ele.checkRoot(); ele.setRoot(root);
this.root?.connect(ele);
}); });
} }
} }
@ -104,6 +104,7 @@ export class Container<E extends EContainerEvent = EContainerEvent>
this.sortedChildren = [...this.children].sort( this.sortedChildren = [...this.children].sort(
(a, b) => a.zIndex - b.zIndex (a, b) => a.zIndex - b.zIndex
); );
this.update();
} }
protected propagateEvent<T extends ActionType>( protected propagateEvent<T extends ActionType>(

View File

@ -437,6 +437,15 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
return canvas; return canvas;
} }
/**
* `requireCanvas` 使
* @param canvas
*/
protected deleteCanvas(canvas: MotaOffscreenCanvas2D) {
if (!this.canvases.delete(canvas)) return;
canvas.delete();
}
//#region 修改元素属性 //#region 修改元素属性
/** /**
@ -457,8 +466,8 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
* @param y * @param y
*/ */
pos(x: number, y: number) { pos(x: number, y: number) {
// 这个函数会调用 update因此不再手动调用 update
this._transform.setTranslate(x, y); this._transform.setTranslate(x, y);
this.update();
} }
/** /**
@ -588,10 +597,15 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
} }
update(item: RenderItem<any> = this): void { update(item: RenderItem<any> = this): void {
if (this.cacheDirty) return; if (this._parent) {
if (this.cacheDirty && this._parent.cacheDirty) return;
this.cacheDirty = true; this.cacheDirty = true;
if (this.hidden) return; if (this.hidden) return;
this.parent?.update(item); this._parent.update(item);
} else {
if (this.cacheDirty) return;
this.cacheDirty = true;
}
} }
updateTransform() { updateTransform() {
@ -650,6 +664,12 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
//#region 父子关系 //#region 父子关系
setRoot(item: RenderItem & IRenderTreeRoot) {
this._root?.disconnect(this);
this._root = item;
item.connect(item);
}
checkRoot(): RenderItem | null { checkRoot(): RenderItem | null {
if (this._root) return this._root; if (this._root) return this._root;
if (this.isRoot) return this; if (this.isRoot) return this;

View File

@ -30,7 +30,10 @@ export class Text extends RenderItem<ETextEvent> {
super(type, false); super(type, false);
this.text = text; this.text = text;
if (text.length > 0) this.calBox(); if (text.length > 0) {
this.calBox();
this.emit('setText', text);
}
} }
protected render( protected render(
@ -70,7 +73,7 @@ export class Text extends RenderItem<ETextEvent> {
setText(text: string) { setText(text: string) {
this.text = text; this.text = text;
this.calBox(); this.calBox();
if (this.parent) this.update(this); this.update(this);
this.emit('setText', text); this.emit('setText', text);
} }
@ -81,7 +84,7 @@ export class Text extends RenderItem<ETextEvent> {
setFont(font: string) { setFont(font: string) {
this.font = font; this.font = font;
this.calBox(); this.calBox();
if (this.parent) this.update(this); this.update(this);
} }
/** /**
@ -110,7 +113,11 @@ export class Text extends RenderItem<ETextEvent> {
this.measure(); this.measure();
this.length = width; this.length = width;
this.descent = actualBoundingBoxAscent; this.descent = actualBoundingBoxAscent;
this.size(width, actualBoundingBoxAscent + actualBoundingBoxDescent); this.size(
width,
Math.abs(actualBoundingBoxAscent) +
Math.abs(actualBoundingBoxDescent)
);
} }
protected handleProps( protected handleProps(
@ -132,7 +139,7 @@ export class Text extends RenderItem<ETextEvent> {
case 'font': case 'font':
if (!this.assertType(nextValue, 'string', key)) return false; if (!this.assertType(nextValue, 'string', key)) return false;
this.setFont(nextValue); this.setFont(nextValue);
break; return true;
case 'strokeWidth': case 'strokeWidth':
this.setStrokeWidth(nextValue); this.setStrokeWidth(nextValue);
return true; return true;
@ -338,14 +345,27 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
} }
} }
interface WinskinPatterns {
top: CanvasPattern;
left: CanvasPattern;
bottom: CanvasPattern;
right: CanvasPattern;
}
export interface EWinskinEvent extends ERenderItemEvent {} export interface EWinskinEvent extends ERenderItemEvent {}
export class Winskin extends RenderItem<EWinskinEvent> { export class Winskin extends RenderItem<EWinskinEvent> {
image: SizedCanvasImageSource; image: SizedCanvasImageSource;
/** 边框宽度 */ /** 边框宽度32表示原始宽度 */
borderSize: number = 32; borderSize: number = 32;
/** 图片名称 */
imageName?: string;
private pendingImage?: ImageIds; private pendingImage?: ImageIds;
private patternCache?: WinskinPatterns;
private patternTransform: DOMMatrix;
private static patternMap: Map<string, WinskinPatterns> = new Map();
constructor( constructor(
image: SizedCanvasImageSource, image: SizedCanvasImageSource,
@ -353,6 +373,63 @@ export class Winskin extends RenderItem<EWinskinEvent> {
) { ) {
super(type, false, false); super(type, false, false);
this.image = image; this.image = image;
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();
const img = this.image;
pattern.size(32, 16);
pattern.withGameScale(false);
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( protected render(
@ -361,143 +438,38 @@ export class Winskin extends RenderItem<EWinskinEvent> {
): void { ): void {
const ctx = canvas.ctx; const ctx = canvas.ctx;
const img = this.image; const img = this.image;
const x = 0; const w = this.width;
const y = 0; const h = this.height;
const w = canvas.width; const pad = this.borderSize / 2;
const h = canvas.height; // 背景
const sz = this.borderSize / 32; ctx.drawImage(img, 0, 0, 128, 128, 2, 2, w - 4, h - 4);
ctx.drawImage(img, 0, 0, 128, 128, x + 2, y + 2, w - 4, h - 4); const pattern = this.getPattern();
ctx.drawImage(img, 128, 0, 16, 16, x, y, 16 * sz, 16 * sz); if (!pattern) return;
let dx; const { top, left, right, bottom } = pattern;
for (dx = 0; dx < w - 64 * sz; dx += 32 * sz) { top.setTransform(this.patternTransform);
ctx.drawImage( left.setTransform(this.patternTransform);
img, right.setTransform(this.patternTransform);
144, bottom.setTransform(this.patternTransform);
0, // 上下左右边框
32, ctx.save();
16, ctx.fillStyle = top;
x + dx + 16, ctx.translate(pad, 0);
y, ctx.fillRect(0, 0, w - pad * 2, pad);
32 * sz, ctx.fillStyle = bottom;
16 * sz ctx.translate(0, h - pad);
); ctx.fillRect(0, 0, w - pad * 2, pad);
ctx.drawImage( ctx.fillStyle = left;
img, ctx.translate(-pad, pad * 2 - h);
144, ctx.fillRect(0, 0, pad, h - pad * 2);
48, ctx.fillStyle = right;
32, ctx.translate(w - pad, 0);
16, ctx.fillRect(0, 0, pad, h - pad * 2);
x + dx + 16, ctx.restore();
y + h - 16 * sz, // 四个角的边框
32 * sz, ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad);
16 * sz ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad);
); ctx.drawImage(img, 128, 48, 16, 16, 0, h - pad, pad, pad);
} ctx.drawImage(img, 176, 48, 16, 16, w - pad, h - pad, pad, pad);
ctx.drawImage(
img,
144,
0,
w - dx - 32,
16,
x + dx + 16 * sz,
y,
w - dx - 32 * sz,
16 * sz
);
ctx.drawImage(
img,
144,
48,
w - dx - 32,
16,
x + dx + 16 * sz,
y + h - 16 * sz,
w - dx - 32 * sz,
16 * sz
);
ctx.drawImage(
img,
176,
0,
16,
16,
x + w - 16 * sz,
y,
16 * sz,
16 * sz
);
// 左右
let dy;
for (dy = 0; dy < h - 64 * sz; dy += 32 * sz) {
ctx.drawImage(
img,
128,
16,
16,
32,
x,
y + dy + 16 * sz,
16 * sz,
32 * sz
);
ctx.drawImage(
img,
176,
16,
16,
32,
x + w - 16 * sz,
y + dy + 16 * sz,
16 * sz,
32 * sz
);
}
ctx.drawImage(
img,
128,
16,
16,
h - dy - 32,
x,
y + dy + 16 * sz,
16 * sz,
h - dy - 32 * sz
);
ctx.drawImage(
img,
176,
16,
16,
h - dy - 32,
x + w - 16 * sz,
y + dy + 16 * sz,
16 * sz,
h - dy - 32 * sz
);
// 下方
ctx.drawImage(
img,
128,
48,
16,
16,
x,
y + h - 16 * sz,
16 * sz,
16 * sz
);
ctx.drawImage(
img,
176,
48,
16,
16,
x + w - 16 * sz,
y + h - 16 * sz,
16 * sz,
16 * sz
);
this.update();
} }
/** /**
@ -506,6 +478,7 @@ export class Winskin extends RenderItem<EWinskinEvent> {
*/ */
setImage(image: SizedCanvasImageSource) { setImage(image: SizedCanvasImageSource) {
this.image = image; this.image = image;
this.patternCache = void 0;
this.update(); this.update();
} }
@ -530,6 +503,7 @@ export class Winskin extends RenderItem<EWinskinEvent> {
} }
this.pendingImage = name; this.pendingImage = name;
} }
this.imageName = name;
} }
/** /**
@ -538,6 +512,8 @@ export class Winskin extends RenderItem<EWinskinEvent> {
*/ */
setBorderSize(size: number) { setBorderSize(size: number) {
this.borderSize = size; this.borderSize = size;
this.patternTransform.a = size / 32;
this.patternTransform.d = size / 32;
this.update(); this.update();
} }

View File

@ -90,6 +90,7 @@
"56": "Method '$1' has been deprecated. Consider using '$2' instead.", "56": "Method '$1' has been deprecated. Consider using '$2' instead.",
"57": "Repeated UI controller on item '$1', new controller will not work.", "57": "Repeated UI controller on item '$1', new controller will not work.",
"58": "Fail to set ellipse round rect, since length of 'ellipse' property should only be 2, 4, 6 or 8. delivered: $1", "58": "Fail to set ellipse round rect, since length of 'ellipse' property should only be 2, 4, 6 or 8. delivered: $1",
"59": "Unknown icon '$1' in parsing text content.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance." "1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
} }

View File

@ -2,8 +2,8 @@ import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { import {
computed, computed,
defineComponent, defineComponent,
nextTick,
onUnmounted, onUnmounted,
onUpdated,
ref, ref,
shallowReactive, shallowReactive,
shallowRef, shallowRef,
@ -13,7 +13,7 @@ import {
} from 'vue'; } from 'vue';
import { logger } from '@/core/common/logger'; import { logger } from '@/core/common/logger';
import { Sprite } from '@/core/render/sprite'; 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 { isNil } from 'lodash-es';
import { SetupComponentOptions } from './types'; import { SetupComponentOptions } from './types';
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
@ -22,7 +22,9 @@ import {
ITextContentConfig, ITextContentConfig,
TextContentTyper, TextContentTyper,
TyperRenderable, TyperRenderable,
TextContentType TextContentType,
WordBreak,
TextAlign
} from './textboxTyper'; } from './textboxTyper';
export interface TextContentProps export interface TextContentProps
@ -34,8 +36,6 @@ export interface TextContentProps
fill?: boolean; fill?: boolean;
/** 是否描边 */ /** 是否描边 */
stroke?: boolean; stroke?: boolean;
/** 是否忽略打字机,直接显伤全部 */
showAll?: boolean;
} }
export type TextContentEmits = { export type TextContentEmits = {
@ -43,6 +43,18 @@ export type TextContentEmits = {
typeStart: () => void; typeStart: () => void;
}; };
export interface TextContentExpose {
/**
*
*/
retype(): void;
/**
*
*/
showAll(): void;
}
const textContentOptions = { const textContentOptions = {
props: [ props: [
'breakChars', 'breakChars',
@ -50,7 +62,6 @@ const textContentOptions = {
'fontSize', 'fontSize',
'fontWeight', 'fontWeight',
'fontItalic', 'fontItalic',
'height',
'ignoreLineEnd', 'ignoreLineEnd',
'ignoreLineStart', 'ignoreLineStart',
'interval', 'interval',
@ -58,17 +69,14 @@ const textContentOptions = {
'lineHeight', 'lineHeight',
'text', 'text',
'textAlign', 'textAlign',
'width',
'wordBreak', 'wordBreak',
'x',
'y',
'fill', 'fill',
'fillStyle', 'fillStyle',
'strokeStyle', 'strokeStyle',
'strokeWidth', 'strokeWidth',
'stroke', 'stroke',
'showAll', 'loc',
'loc' 'width'
], ],
emits: ['typeEnd', 'typeStart'] emits: ['typeEnd', 'typeStart']
} satisfies SetupComponentOptions< } satisfies SetupComponentOptions<
@ -81,52 +89,51 @@ export const TextContent = defineComponent<
TextContentProps, TextContentProps,
TextContentEmits, TextContentEmits,
keyof TextContentEmits keyof TextContentEmits
>((props, { emit }) => { >((props, { emit, expose }) => {
if (props.width && props.width <= 0) { const width = computed(() => props.width ?? props.loc?.[2] ?? 200);
if (width.value < 0) {
logger.warn(41, String(props.width)); logger.warn(41, String(props.width));
} }
const typer = new TextContentTyper(props); const typer = new TextContentTyper(props);
let renderable: TyperRenderable[] = []; let renderable: TyperRenderable[] = [];
let needUpdate = false; let needUpdate = false;
let nowText = '';
watch(props, value => {
typer.setConfig(value);
});
const retype = () => { const retype = () => {
if (props.showAll) {
typer.typeAll();
}
if (props.text === nowText) return;
if (needUpdate) return; if (needUpdate) return;
needUpdate = true; needUpdate = true;
if (!spriteElement.value) { if (!spriteElement.value) {
needUpdate = false; needUpdate = false;
} }
nowText = props.text ?? '';
renderable = []; renderable = [];
spriteElement.value?.requestBeforeFrame(() => { spriteElement.value?.requestBeforeFrame(() => {
typer.setConfig(props); typer.setConfig(props);
typer.setText(props.text ?? ''); typer.setText(props.text ?? '');
typer.type(); typer.type();
if (props.showAll) {
typer.typeAll();
}
needUpdate = false; needUpdate = false;
}); });
}; };
onUpdated(retype); const showAll = () => {
typer.typeAll();
};
watch(props, value => {
typer.setConfig(value);
retype();
});
expose({ retype, showAll });
const spriteElement = shallowRef<Sprite>(); const spriteElement = shallowRef<Sprite>();
const renderContent = (canvas: MotaOffscreenCanvas2D) => { const renderContent = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx; const ctx = canvas.ctx;
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
renderable.forEach(v => { renderable.forEach(v => {
if (v.type === TextContentType.Text) { switch (v.type) {
case TextContentType.Text: {
if (v.text.length === 0) return;
ctx.fillStyle = v.fillStyle; ctx.fillStyle = v.fillStyle;
ctx.strokeStyle = v.strokeStyle; ctx.strokeStyle = v.strokeStyle;
ctx.font = v.font; ctx.font = v.font;
@ -138,12 +145,16 @@ export const TextContent = defineComponent<
if (props.stroke) { if (props.stroke) {
ctx.strokeText(text, v.x, v.y); ctx.strokeText(text, v.x, v.y);
} }
} else { break;
const r = v.renderable; }
case TextContentType.Icon: {
const { renderable: r, x: dx, y: dy, width, height } = v;
const render = r.render; const render = r.render;
const [x, y, w, h] = render[0]; const [x, y, w, h] = render[0];
const icon = r.autotile ? r.image[0] : r.image; const icon = r.autotile ? r.image[0] : r.image;
ctx.drawImage(icon, x, y, w, h, v.x, v.y, v.width, v.height); ctx.drawImage(icon, x, y, w, h, dx, dy, width, height);
break;
}
} }
}); });
}; };
@ -164,19 +175,15 @@ export const TextContent = defineComponent<
return () => { return () => {
return ( return (
<sprite <sprite
{...props} loc={props.loc}
ref={spriteElement} ref={spriteElement}
x={props.x}
y={props.y}
width={props.width}
height={props.height}
render={renderContent} render={renderContent}
></sprite> ></sprite>
); );
}; };
}, textContentOptions); }, textContentOptions);
export interface TextboxProps extends TextContentProps, ContainerProps { export interface TextboxProps extends TextContentProps, DefaultProps {
/** 背景颜色 */ /** 背景颜色 */
backColor?: CanvasStyle; backColor?: CanvasStyle;
/** 背景 winskin */ /** 背景 winskin */
@ -195,6 +202,28 @@ export interface TextboxProps extends TextContentProps, ContainerProps {
titlePadding?: number; titlePadding?: number;
} }
export interface TextboxExpose {
/**
*
*/
show(): void;
/**
*
*/
hide(): void;
/**
*
*/
retype(): void;
/**
*
*/
showAll(): void;
}
type TextboxEmits = TextContentEmits; type TextboxEmits = TextContentEmits;
type TextboxSlots = SlotsType<{ type TextboxSlots = SlotsType<{
default: (data: TextboxProps) => VNode[]; default: (data: TextboxProps) => VNode[];
@ -205,23 +234,14 @@ const textboxOptions = {
props: (textContentOptions.props as (keyof TextboxProps)[]).concat([ props: (textContentOptions.props as (keyof TextboxProps)[]).concat([
'backColor', 'backColor',
'winskin', 'winskin',
'id',
'padding', 'padding',
'alpha',
'hidden',
'anchorX',
'anchorY',
'anti',
'cache',
'composite',
'fall',
'hd',
'transform',
'type',
'zIndex',
'titleFill', 'titleFill',
'titleStroke', 'titleStroke',
'titleFont' 'titleFont',
'titlePadding',
'id',
'hidden',
'title'
]), ]),
emits: textContentOptions.emits emits: textContentOptions.emits
} satisfies SetupComponentOptions<TextboxProps, {}, string, TextboxSlots>; } satisfies SetupComponentOptions<TextboxProps, {}, string, TextboxSlots>;
@ -236,153 +256,206 @@ export const Textbox = defineComponent<
TextboxEmits, TextboxEmits,
keyof TextboxEmits, keyof TextboxEmits,
TextboxSlots TextboxSlots
>((props, { slots }) => { >((props, { slots, expose }) => {
const data = shallowReactive({ ...props }); const contentData = shallowReactive<TextContentProps>({});
data.padding ??= 8; const data = shallowReactive<TextboxProps>({});
data.width ??= 200;
data.height ??= 200; const setContentData = () => {
data.id ??= ''; contentData.breakChars = props.breakChars ?? '';
data.alpha ??= 1; contentData.fontFamily = props.fontFamily ?? 'Verdana';
data.titleFill ??= '#000'; contentData.fontSize = props.fontSize ?? 16;
data.titleStroke ??= 'transparent'; contentData.fontWeight = props.fontWeight ?? 500;
data.titleFont ??= '16px Verdana'; contentData.fontItalic = props.fontItalic ?? false;
data.titlePadding ??= 4; 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<Text>(); const titleElement = ref<Text>();
const titleWidth = ref(data.titlePadding * 2); const content = ref<TextContentExpose>();
const titleHeight = ref(data.titlePadding * 2); const hidden = ref(props.hidden);
/** 标题宽度 */
const tw = ref(data.titlePadding! * 2);
/** 标题高度 */
const th = ref(data.titlePadding! * 2);
const contentY = computed(() => { const contentY = computed(() => {
const height = titleHeight.value; const height = th.value;
return data.title ? height : 0; return data.title ? height : 0;
}); });
const backHeight = computed(() => data.height! - contentY.value);
const contentWidth = computed(() => data.width! - data.padding! * 2); const contentWidth = computed(() => data.width! - data.padding! * 2);
const contentHeight = computed( const contentHeight = computed(
() => data.height! - data.padding! * 2 - contentY.value () => data.height! - data.padding! * 2 - contentY.value
); );
const calTitleSize = (text: string) => { const onSetText = () => {
if (!titleElement.value) return; nextTick(() => {
titleElement.value?.requestBeforeFrame(() => {
if (titleElement.value) {
const { width, height } = titleElement.value; const { width, height } = titleElement.value;
titleWidth.value = width + data.titlePadding! * 2; tw.value = width + data.padding! * 2;
titleHeight.value = height + data.titlePadding! * 2; th.value = height + data.padding! * 2;
data.title = text; }
});
});
}; };
watch(titleElement, (value, old) => {
old?.off('setText', calTitleSize);
value?.on('setText', calTitleSize);
if (value) calTitleSize(value?.text);
});
onUnmounted(() => {
titleElement.value?.off('setText', calTitleSize);
});
// ----- store // ----- store
/** 结束打字机 */ /** 结束打字机 */
const storeEmits: TextboxStoreEmits = { const storeEmits: TextboxStoreEmits = {
endType() { 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( const store = TextboxStore.use(
props.id ?? getNextTextboxId(), props.id ?? getNextTextboxId(),
data, contentData,
storeEmits 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 = () => { const onTypeStart = () => {
store.emitTypeStart(); store.emitTypeStart();
}; };
const onTypeEnd = () => { const onTypeEnd = () => {
data.showAll = false;
store.emitTypeEnd(); store.emitTypeEnd();
}; };
return () => { expose<TextboxExpose>({
return ( show() {
<container {...data} hidden={hidden.value} alpha={data.alpha}> hidden.value = false;
{data.title ? ( },
hide() {
hidden.value = true;
},
retype() {
content.value?.retype();
},
showAll() {
content.value?.showAll();
}
});
return () => (
<container <container
zIndex={10} id={props.id}
width={titleWidth.value} hidden={hidden.value}
height={titleHeight.value} alpha={data.alpha}
loc={props.loc}
> >
{data.title && (
<container zIndex={10} loc={[0, 0, tw.value, th.value]}>
{slots.title ? ( {slots.title ? (
slots.title(data) slots.title(data)
) : props.winskin ? ( ) : props.winskin ? (
<winskin image={props.winskin}></winskin> <winskin
image={props.winskin}
loc={[0, 0, tw.value, th.value]}
></winskin>
) : ( ) : (
<g-rect <g-rect loc={[0, 0, tw.value, th.value]}></g-rect>
x={0}
y={0}
width={titleWidth.value}
height={titleHeight.value}
fillStyle={data.backColor}
></g-rect>
)} )}
<text <text
ref={titleElement} ref={titleElement}
text={data.title} text={data.title}
x={data.titlePadding} loc={[data.titlePadding, data.titlePadding]}
y={data.titlePadding}
fillStyle={data.titleFill} fillStyle={data.titleFill}
strokeStyle={data.titleStroke} strokeStyle={data.titleStroke}
font={data.titleFont} font={data.titleFont}
></text> ></text>
</container> </container>
) : (
''
)} )}
{slots.default ? ( {slots.default ? (
slots.default(data) slots.default(data)
) : props.winskin ? ( ) : props.winskin ? (
<winskin image={props.winskin}></winskin> <winskin
image={props.winskin}
loc={[0, contentY.value, data.width!, backHeight.value]}
></winskin>
) : ( ) : (
<g-rect <g-rect
x={0} loc={[0, contentY.value, data.width!, backHeight.value]}
y={contentY.value}
width={data.width ?? 200}
height={(data.height ?? 200) - contentY.value}
fill fill
fillStyle={data.backColor} fillStyle={data.backColor}
></g-rect> ></g-rect>
)} )}
<TextContent <TextContent
{...data} {...contentData}
id="" ref={content}
hidden={false}
x={data.padding!} x={data.padding!}
y={contentY.value + data.padding!} y={contentY.value + data.padding!}
width={contentWidth.value} width={contentWidth.value}
height={contentHeight.value} height={contentHeight.value}
onTypeEnd={onTypeEnd} onTypeEnd={onTypeEnd}
onTypeStart={onTypeStart} onTypeStart={onTypeStart}
zIndex={0}
showAll={data.showAll}
></TextContent> ></TextContent>
</container> </container>
); );
};
}, textboxOptions); }, textboxOptions);
interface TextboxStoreEmits { interface TextboxStoreEmits {
endType: () => void; endType: () => void;
hide: () => void;
show: () => void;
update: (value: TextboxProps) => void;
setText: (text: string) => void;
} }
interface TextboxStoreEvent { interface TextboxStoreEvent {
@ -436,21 +509,30 @@ export class TextboxStore extends EventEmitter<TextboxStoreEvent> {
// @ts-expect-error 无法推导 // @ts-expect-error 无法推导
if (!isNil(value)) this.data[key] = value; if (!isNil(value)) this.data[key] = value;
} }
this.emits.update(this.data);
this.emit('update', this.data); this.emit('update', this.data);
} }
/**
*
* @param text
*/
setText(text: string) {
this.emits.setText(text);
}
/** /**
* *
*/ */
show() { show() {
this.emit('show'); this.emits.show();
} }
/** /**
* *
*/ */
hide() { hide() {
this.emit('hide'); this.emits.hide();
} }
/** /**

View File

@ -135,7 +135,16 @@ export interface TyperIconRenderable {
renderable: RenderableData | AutotileRenderable; renderable: RenderableData | AutotileRenderable;
} }
export type TyperRenderable = TyperTextRenderable | TyperIconRenderable; export interface TyperWaitRenderable {
type: TextContentType.Wait;
wait: number;
waited: number;
}
export type TyperRenderable =
| TyperTextRenderable
| TyperIconRenderable
| TyperWaitRenderable;
interface TextContentTyperEvent { interface TextContentTyperEvent {
typeStart: []; typeStart: [];
@ -271,17 +280,20 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
private createTyperData(index: number, line: number) { private createTyperData(index: number, line: number) {
const renderable = this.renderObject.data[index]; const renderable = this.renderObject.data[index];
if (!renderable) return false; if (!renderable) return false;
if (renderable.type === TextContentType.Text) { switch (renderable.type) {
case TextContentType.Text: {
if (line < 0 || line > renderable.splitLines.length) { if (line < 0 || line > renderable.splitLines.length) {
return false; return false;
} }
const start = renderable.splitLines[line - 1] ?? 0; const start = renderable.splitLines[line - 1] ?? -1;
const end = renderable.splitLines[line] ?? renderable.text.length; const end =
renderable.splitLines[line] ?? renderable.text.length - 1;
const data: TyperTextRenderable = { const data: TyperTextRenderable = {
type: TextContentType.Text, type: TextContentType.Text,
x: this.x, x: this.x,
y: this.y, y: this.y,
text: renderable.text.slice(start, end), text: renderable.text.slice(start + 1, end + 1),
font: renderable.font, font: renderable.font,
fillStyle: renderable.fillStyle, fillStyle: renderable.fillStyle,
strokeStyle: this.config.strokeStyle, strokeStyle: this.config.strokeStyle,
@ -290,7 +302,8 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
this.processingData = data; this.processingData = data;
this.renderData.push(data); this.renderData.push(data);
return true; return true;
} else { }
case TextContentType.Icon: {
const tex = texture.getRenderable(renderable.icon!); const tex = texture.getRenderable(renderable.icon!);
if (!tex) return false; if (!tex) return false;
const { render } = tex; const { render } = tex;
@ -315,6 +328,17 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
this.renderData.push(data); this.renderData.push(data);
return true; return true;
} }
case TextContentType.Wait: {
const data: TyperWaitRenderable = {
type: TextContentType.Wait,
wait: renderable.wait!,
waited: 0
};
this.processingData = data;
this.renderData.push(data);
return true;
}
}
} }
/** /**
@ -339,7 +363,8 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
return true; return true;
} }
const lineHeight = this.renderObject.lineHeights[this.nowLine]; const lineHeight = this.renderObject.lineHeights[this.nowLine];
if (now.type === TextContentType.Text) { switch (now.type) {
case TextContentType.Text: {
const restChars = now.text.length - now.pointer; const restChars = now.text.length - now.pointer;
if (restChars <= rest) { if (restChars <= rest) {
// 当前这段 renderable 打字完成后,刚好结束或还有内容 // 当前这段 renderable 打字完成后,刚好结束或还有内容
@ -356,28 +381,20 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
this.x += renderable.lastLineWidth; this.x += renderable.lastLineWidth;
this.dataLine = 0; this.dataLine = 0;
this.pointer++; this.pointer++;
const success = this.createTyperData(
this.pointer,
this.dataLine
);
if (!success) return true;
} else { } else {
// 不是最后一行,那么换行 // 不是最后一行,那么换行
this.x = 0; this.x = 0;
this.y += lineHeight + this.config.lineHeight; this.y += lineHeight + this.config.lineHeight;
this.dataLine++; this.dataLine++;
this.nowLine++; this.nowLine++;
const success = this.createTyperData(
this.pointer,
this.dataLine
);
if (!success) return true;
} }
} else { } else {
now.pointer += rest; now.pointer += rest;
return false; return false;
} }
} else { break;
}
case TextContentType.Icon: {
rest--; rest--;
this.pointer++; this.pointer++;
if (renderable.splitLines[0] === 0) { if (renderable.splitLines[0] === 0) {
@ -388,14 +405,23 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
this.dataLine = 0; this.dataLine = 0;
now.x = 0; now.x = 0;
now.y = this.y; now.y = this.y;
} else {
this.x += now.width;
} }
const success = this.createTyperData( break;
this.pointer, }
this.dataLine case TextContentType.Wait: {
); now.waited += num;
if (now.waited > now.wait) {
// 等待结束
this.pointer++;
}
break;
}
}
const success = this.createTyperData(this.pointer, this.dataLine);
if (!success) return true; if (!success) return true;
} }
}
return false; return false;
} }
@ -422,6 +448,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
* *
*/ */
type() { type() {
if (this.typing) return;
if (this.config.interval === 0) { if (this.config.interval === 0) {
this.emit('typeStart'); this.emit('typeStart');
this.typeChars(Infinity); this.typeChars(Infinity);
@ -438,6 +465,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
* *
*/ */
typeAll() { typeAll() {
if (!this.typing) return;
this.typeChars(Infinity); this.typeChars(Infinity);
this.render?.(this.renderData, false); this.render?.(this.renderData, false);
} }
@ -561,7 +589,7 @@ export class TextContentParser {
const end = this.indexParam(start); const end = this.indexParam(start);
if (end === -1) { if (end === -1) {
// 标签结束 // 标签结束
return ['', start]; return ['', start - 1];
} else { } else {
// 标签开始 // 标签开始
return [this.text.slice(start + 1, end), end]; return [this.text.slice(start + 1, end), end];
@ -632,43 +660,43 @@ export class TextContentParser {
} }
private parseFillStyle(pointer: number) { private parseFillStyle(pointer: number) {
const [param, end] = this.getChildableTagParam(pointer + 1); const [param, end] = this.getChildableTagParam(pointer + 2);
if (!param) { if (!param) {
// 参数为空或没有参数,视为标签结束 // 参数为空或没有参数,视为标签结束
const color = this.fillStyleStack.pop(); const color = this.fillStyleStack.pop();
if (!color) { if (!color) {
logger.warn(54, '\\r', pointer.toString()); logger.warn(54, '\\r', pointer.toString());
return pointer; return end;
} }
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fillStyle = color; this.status.fillStyle = color;
return pointer; return end;
} else { } else {
// 标签开始 // 标签开始
this.fillStyleStack.push(this.status.fillStyle); this.fillStyleStack.push(this.status.fillStyle);
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fillStyle = param; this.status.fillStyle = param;
return end; return end;
} }
} }
private parseFontSize(pointer: number) { private parseFontSize(pointer: number) {
const [param, end] = this.getChildableTagParam(pointer + 1); const [param, end] = this.getChildableTagParam(pointer + 2);
if (!param) { if (!param) {
// 参数为空或没有参数,视为标签结束 // 参数为空或没有参数,视为标签结束
const size = this.fontSizeStack.pop(); const size = this.fontSizeStack.pop();
if (!size) { if (!size) {
logger.warn(54, '\\c', pointer.toString()); logger.warn(54, '\\c', pointer.toString());
return pointer; return end;
} }
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontSize = size; this.status.fontSize = size;
this.font = this.buildFont(); this.font = this.buildFont();
return pointer; return end;
} else { } else {
// 标签开始 // 标签开始
this.fontSizeStack.push(this.status.fontSize); this.fontSizeStack.push(this.status.fontSize);
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontSize = parseFloat(param); this.status.fontSize = parseFloat(param);
this.font = this.buildFont(); this.font = this.buildFont();
return end; return end;
@ -676,22 +704,22 @@ export class TextContentParser {
} }
private parseFontFamily(pointer: number) { private parseFontFamily(pointer: number) {
const [param, end] = this.getChildableTagParam(pointer + 1); const [param, end] = this.getChildableTagParam(pointer + 2);
if (!param) { if (!param) {
// 参数为空或没有参数,视为标签结束 // 参数为空或没有参数,视为标签结束
const font = this.fontFamilyStack.pop(); const font = this.fontFamilyStack.pop();
if (!font) { if (!font) {
logger.warn(54, '\\g', pointer.toString()); logger.warn(54, '\\g', pointer.toString());
return pointer; return end;
} }
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontFamily = font; this.status.fontFamily = font;
this.font = this.buildFont(); this.font = this.buildFont();
return pointer; return end;
} else { } else {
// 标签开始 // 标签开始
this.fontFamilyStack.push(this.status.fontFamily); this.fontFamilyStack.push(this.status.fontFamily);
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontFamily = param; this.status.fontFamily = param;
this.font = this.buildFont(); this.font = this.buildFont();
return end; return end;
@ -699,20 +727,20 @@ export class TextContentParser {
} }
private parseFontWeight() { private parseFontWeight() {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontWeight = this.status.fontWeight > 500 ? 500 : 700; this.status.fontWeight = this.status.fontWeight > 500 ? 500 : 700;
this.font = this.buildFont(); this.font = this.buildFont();
} }
private parseFontItalic() { private parseFontItalic() {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
this.status.fontItalic = !this.status.fontItalic; this.status.fontItalic = !this.status.fontItalic;
this.font = this.buildFont(); this.font = this.buildFont();
} }
private parseWait(pointer: number) { private parseWait(pointer: number) {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
const [param, end] = this.getTagParam(pointer); const [param, end] = this.getTagParam(pointer + 2);
if (!param) { if (!param) {
logger.warn(55, '\\z'); logger.warn(55, '\\z');
return pointer; return pointer;
@ -723,10 +751,10 @@ export class TextContentParser {
} }
private parseIcon(pointer: number) { private parseIcon(pointer: number) {
this.addTextRenderable(); if (this.resolved.length > 0) this.addTextRenderable();
const [param, end] = this.getTagParam(pointer); const [param, end] = this.getTagParam(pointer + 2);
if (!param) { if (!param) {
logger.warn(55, '\\z'); logger.warn(55, '\\i');
return pointer; return pointer;
} }
if (/^\d+$/.test(param)) { if (/^\d+$/.test(param)) {
@ -739,6 +767,10 @@ export class TextContentParser {
this.addIconRenderable(num as AllNumbers); this.addIconRenderable(num as AllNumbers);
} else { } else {
const num = texture.idNumberMap[param as AllIds]; const num = texture.idNumberMap[param as AllIds];
if (num === void 0) {
logger.warn(59, param);
return end;
}
this.addIconRenderable(num); this.addIconRenderable(num);
} }
} }
@ -842,9 +874,11 @@ export class TextContentParser {
break; break;
case 'd': case 'd':
this.parseFontWeight(); this.parseFontWeight();
pointer++;
break; break;
case 'e': case 'e':
this.parseFontItalic(); this.parseFontItalic();
pointer++;
break; break;
case 'z': case 'z':
pointer = this.parseWait(pointer); pointer = this.parseWait(pointer);
@ -861,7 +895,7 @@ export class TextContentParser {
// 表达式 // 表达式
pointer++; pointer++;
inExpression = true; inExpression = true;
expStart = pointer; expStart = pointer + 1;
continue; continue;
} }
@ -875,7 +909,8 @@ export class TextContentParser {
private getHeight(metrics: TextMetrics) { private getHeight(metrics: TextMetrics) {
return ( return (
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent Math.abs(metrics.actualBoundingBoxAscent) +
Math.abs(metrics.actualBoundingBoxDescent)
); );
} }
@ -899,10 +934,14 @@ export class TextContentParser {
// 如果大于猜测,那么算长度 // 如果大于猜测,那么算长度
const data = this.renderable[this.nowRenderable]; const data = this.renderable[this.nowRenderable];
const ctx = this.testCanvas.ctx; const ctx = this.testCanvas.ctx;
ctx.font = this.font; ctx.font = data.font;
const metrics = ctx.measureText( const metrics = ctx.measureText(
data.text.slice(this.lineStart, pointer + 1) data.text.slice(this.lineStart, pointer + 1)
); );
const height = this.getHeight(metrics);
if (height > this.lineHeight) {
this.lineHeight = height;
}
if (metrics.width < rest) { if (metrics.width < rest) {
// 实际宽度小于剩余宽度时,将猜测增益乘以剩余总宽度与当前宽度的比值的若干倍 // 实际宽度小于剩余宽度时,将猜测增益乘以剩余总宽度与当前宽度的比值的若干倍
this.guessGain *= (rest / metrics.width) * (1.1 + 1 / length); this.guessGain *= (rest / metrics.width) * (1.1 + 1 / length);
@ -947,7 +986,7 @@ export class TextContentParser {
const data = this.renderable[index]; const data = this.renderable[index];
const { wordBreak } = data; const { wordBreak } = data;
const ctx = this.testCanvas.ctx; const ctx = this.testCanvas.ctx;
ctx.font = this.font; ctx.font = data.font;
while (true) { while (true) {
const mid = Math.floor((start + end) / 2); const mid = Math.floor((start + end) / 2);
if (mid === start) { if (mid === start) {
@ -958,31 +997,33 @@ export class TextContentParser {
} }
const text = data.text.slice( const text = data.text.slice(
wordBreak[this.bsStart], wordBreak[this.bsStart],
wordBreak[mid] wordBreak[mid] + 1
); );
const metrics = ctx.measureText(text); const metrics = ctx.measureText(text);
height = this.getHeight(metrics);
if (metrics.width > width) { if (metrics.width > width) {
end = mid; end = mid;
} else if (metrics.width === width) { } else if (metrics.width === width) {
if (height > this.lineHeight) {
this.lineHeight = height;
}
return mid; return mid;
} else { } else {
start = mid; start = mid;
} }
height = this.getHeight(metrics);
} }
} }
/** /**
* *
*/ */
private checkRestLine(width: number, guess: number) { private checkRestLine(width: number, guess: number, pointer: number) {
if (this.wordBreak.length === 0) return true; if (this.wordBreak.length === 0) return true;
const last = this.nowRenderable - 1; if (pointer === -1) {
if (last === -1) { return this.checkLineWidth(width, guess, 0);
const now = this.renderable[this.nowRenderable];
return this.checkLineWidth(width, guess, now.text.length);
} }
const data = this.renderable[last]; const isLast = this.renderable.length - 1 === pointer;
const data = this.renderable[pointer];
const rest = width - this.lineWidth; const rest = width - this.lineWidth;
if (data.type === TextContentType.Text) { if (data.type === TextContentType.Text) {
const wordBreak = data.wordBreak; const wordBreak = data.wordBreak;
@ -990,7 +1031,7 @@ export class TextContentParser {
const lastIndex = isNil(lastLine) ? 0 : lastLine; const lastIndex = isNil(lastLine) ? 0 : lastLine;
const restText = data.text.slice(lastIndex); const restText = data.text.slice(lastIndex);
const ctx = this.testCanvas.ctx; const ctx = this.testCanvas.ctx;
ctx.font = this.font; ctx.font = data.font;
const metrics = ctx.measureText(restText); const metrics = ctx.measureText(restText);
// 如果剩余内容不能构成完整的行 // 如果剩余内容不能构成完整的行
if (metrics.width < rest) { if (metrics.width < rest) {
@ -1008,12 +1049,12 @@ export class TextContentParser {
this.bsEnd = lastBreak; this.bsEnd = lastBreak;
let maxWidth = rest; let maxWidth = rest;
while (true) { while (true) {
const index = this.bsLineWidth(maxWidth, last); const index = this.bsLineWidth(maxWidth, pointer);
data.splitLines.push(this.wordBreak[index]); data.splitLines.push(this.wordBreak[index]);
this.lineHeights.push(this.lineHeight); this.lineHeights.push(this.lineHeight);
this.bsStart = index; this.bsStart = index;
const text = data.text.slice(this.wordBreak[index]); const text = data.text.slice(this.wordBreak[index]);
if (text.length < guess / 4) { if (!isLast && text.length < guess / 4) {
// 如果剩余文字很少,几乎不可能会单独成一行时,直接结束循环 // 如果剩余文字很少,几乎不可能会单独成一行时,直接结束循环
this.lastBreakIndex = index; this.lastBreakIndex = index;
break; break;
@ -1040,9 +1081,15 @@ export class TextContentParser {
} else { } else {
iconWidth = this.status.fontSize; iconWidth = this.status.fontSize;
} }
this.lineWidth += iconWidth;
const iconHeight = iconWidth / aspect;
if (iconHeight > this.lineHeight) {
this.lineHeight = iconHeight;
}
if (iconWidth > rest) { if (iconWidth > rest) {
data.splitLines.push(0); data.splitLines.push(0);
this.lineHeights.push(this.lineHeight); this.lineHeights.push(this.lineHeight);
this.lineWidth = 0;
return true; return true;
} else { } else {
return false; return false;
@ -1070,8 +1117,6 @@ export class TextContentParser {
const allBreak = this.wordBreakRule === WordBreak.All; const allBreak = this.wordBreakRule === WordBreak.All;
// debugger;
for (let i = 0; i < this.renderable.length; i++) { for (let i = 0; i < this.renderable.length; i++) {
const data = this.renderable[i]; const data = this.renderable[i];
const { wordBreak, fontSize } = data; const { wordBreak, fontSize } = data;
@ -1080,7 +1125,7 @@ export class TextContentParser {
this.wordBreak = wordBreak; this.wordBreak = wordBreak;
if (data.type === TextContentType.Icon) { if (data.type === TextContentType.Icon) {
this.checkRestLine(width, guess); this.checkRestLine(width, guess, i - 1);
continue; continue;
} else if (data.type === TextContentType.Wait) { } else if (data.type === TextContentType.Wait) {
continue; continue;
@ -1122,7 +1167,7 @@ export class TextContentParser {
} }
} }
this.checkRestLine(width, guess); this.checkRestLine(width, guess, i);
} }
return { return {

View File

@ -4,6 +4,7 @@ import { defineComponent } from 'vue';
import { UIController } from '@/core/system'; import { UIController } from '@/core/system';
import { mainSceneUI } from './ui/main'; import { mainSceneUI } from './ui/main';
import { MAIN_HEIGHT, MAIN_WIDTH } from './shared'; import { MAIN_HEIGHT, MAIN_WIDTH } from './shared';
import { TextboxStore } from './components';
export function create() { export function create() {
const main = new MotaRenderer(); const main = new MotaRenderer();
@ -34,6 +35,10 @@ export function create() {
console.log(main); console.log(main);
} }
Mota.register('module', 'MainUI', {
TextboxStore
});
export * from './components'; export * from './components';
export * from './ui'; export * from './ui';
export * from './use'; export * from './use';

View File

@ -57,9 +57,7 @@ const MainScene = defineComponent(() => {
const mainTextboxProps: Props<typeof Textbox> = { const mainTextboxProps: Props<typeof Textbox> = {
text: '', text: '',
hidden: true, hidden: true,
width: 480, loc: [0, 330, 480, 150],
height: 150,
y: 330,
zIndex: 30, zIndex: 30,
fillStyle: '#fff', fillStyle: '#fff',
titleFill: 'gold', titleFill: 'gold',
@ -67,7 +65,7 @@ const MainScene = defineComponent(() => {
titleFont: '700 20px normal', titleFont: '700 20px normal',
winskin: 'winskin2.png', winskin: 'winskin2.png',
interval: 100, interval: 100,
lineHeight: 6 lineHeight: 4
}; };
const map = ref<LayerGroup>(); const map = ref<LayerGroup>();