mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-31 15:09:26 +08:00
feat: Textbox & fix: hide & show
This commit is contained in:
parent
b018a5fd9a
commit
297b67da5b
@ -1,9 +1,12 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
ref,
|
||||
shallowReactive,
|
||||
shallowRef,
|
||||
SlotsType,
|
||||
VNode,
|
||||
@ -16,6 +19,8 @@ import { Sprite } from '../sprite';
|
||||
import { onTick } from '../renderer';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { SetupComponentOptions } from './types';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { Container } from '../container';
|
||||
|
||||
export const enum WordBreak {
|
||||
/** 不换行 */
|
||||
@ -128,13 +133,25 @@ const textContentOptions = {
|
||||
'width',
|
||||
'wordBreak',
|
||||
'x',
|
||||
'y'
|
||||
'y',
|
||||
'fill',
|
||||
'fillStyle',
|
||||
'strokeStyle',
|
||||
'strokeWidth',
|
||||
'stroke'
|
||||
],
|
||||
emits: ['typeEnd', 'typeStart']
|
||||
} satisfies SetupComponentOptions<TextContentProps, TextContentEmits>;
|
||||
} satisfies SetupComponentOptions<
|
||||
TextContentProps,
|
||||
TextContentEmits,
|
||||
keyof TextContentEmits
|
||||
>;
|
||||
|
||||
export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
||||
(props, { emit }) => {
|
||||
export const TextContent = defineComponent<
|
||||
TextContentProps,
|
||||
TextContentEmits,
|
||||
keyof TextContentEmits
|
||||
>((props, { emit }) => {
|
||||
if (props.width && props.width <= 0) {
|
||||
logger.warn(41, String(props.width));
|
||||
}
|
||||
@ -145,10 +162,7 @@ export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
||||
y: props.y ?? 0,
|
||||
width: !props.width || props.width <= 0 ? 200 : props.width,
|
||||
height: props.height ?? 200,
|
||||
font:
|
||||
props.font ??
|
||||
core.status.globalAttribute?.font ??
|
||||
'16px Verdana',
|
||||
font: props.font ?? core.status.globalAttribute?.font ?? '16px Verdana',
|
||||
ignoreLineEnd: props.ignoreLineEnd ?? new Set(),
|
||||
ignoreLineStart: props.ignoreLineStart ?? new Set(),
|
||||
keepLast: props.keepLast ?? false,
|
||||
@ -227,8 +241,7 @@ export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
||||
spriteElement.value?.update();
|
||||
const time = Date.now();
|
||||
const char =
|
||||
Math.floor((time - startTime) / renderData.interval!) +
|
||||
fromChar;
|
||||
Math.floor((time - startTime) / renderData.interval!) + fromChar;
|
||||
if (!isFinite(char)) {
|
||||
renderable.forEach(v => (v.pointer = v.text.length));
|
||||
needUpdate = false;
|
||||
@ -272,8 +285,18 @@ export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
||||
renderable.forEach(v => {
|
||||
if (v.pointer === 0) return;
|
||||
const text = v.text.slice(0, v.pointer);
|
||||
if (renderData.textAlign === TextAlign.Left) {
|
||||
if (renderData.stroke) ctx.strokeText(text, v.x, v.y);
|
||||
if (renderData.fill) ctx.fillText(text, v.x, v.y);
|
||||
} else if (renderData.textAlign === TextAlign.Center) {
|
||||
const x = (renderData.width - v.x) / 2 + v.x;
|
||||
if (renderData.stroke) ctx.strokeText(text, x, v.y);
|
||||
if (renderData.fill) ctx.fillText(text, x, v.y);
|
||||
} else {
|
||||
const x = renderData.width;
|
||||
if (renderData.stroke) ctx.strokeText(text, x, v.y);
|
||||
if (renderData.fill) ctx.fillText(text, x, v.y);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -406,12 +429,7 @@ export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
||||
rawRender(data.value.text, value);
|
||||
} else {
|
||||
const index = old.length - 1;
|
||||
continueRender(
|
||||
data.value.text,
|
||||
oldLast,
|
||||
value,
|
||||
index
|
||||
);
|
||||
continueRender(data.value.text, oldLast, value, index);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -427,7 +445,7 @@ export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
||||
<sprite
|
||||
ref={spriteElement}
|
||||
hd
|
||||
antiAliasing={false}
|
||||
antiAliasing={true}
|
||||
x={renderData.x}
|
||||
y={renderData.y}
|
||||
width={renderData.width}
|
||||
@ -436,35 +454,66 @@ export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
||||
></sprite>
|
||||
);
|
||||
};
|
||||
},
|
||||
textContentOptions
|
||||
);
|
||||
}, textContentOptions);
|
||||
|
||||
export interface TextboxProps extends TextContentProps {
|
||||
id?: string;
|
||||
/** 背景颜色 */
|
||||
backColor?: CanvasStyle;
|
||||
/** 背景 winskin */
|
||||
winskin?: string;
|
||||
/** 边框与文字间的距离,默认为8 */
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
interface TextboxSlots extends SlotsType {
|
||||
default: () => VNode;
|
||||
}
|
||||
type TextboxEmits = TextContentEmits;
|
||||
type TextboxSlots = SlotsType<{ default: (data: TextboxProps) => VNode[] }>;
|
||||
|
||||
const textboxOptions = {
|
||||
props: (textContentOptions.props as (keyof TextboxProps)[]).concat([
|
||||
'backColor',
|
||||
'winskin'
|
||||
])
|
||||
'winskin',
|
||||
'id'
|
||||
]),
|
||||
emits: textContentOptions.emits
|
||||
} satisfies SetupComponentOptions<TextboxProps, {}, string, TextboxSlots>;
|
||||
|
||||
export const Textbox = defineComponent<TextboxProps, {}, string, TextboxSlots>(
|
||||
(props, { slots }) => {
|
||||
let id = 0;
|
||||
function getNextTextboxId() {
|
||||
return `@default-textbox-${id++}`;
|
||||
}
|
||||
|
||||
export const Textbox = defineComponent<
|
||||
TextboxProps,
|
||||
TextboxEmits,
|
||||
keyof TextboxEmits,
|
||||
TextboxSlots
|
||||
>((props, { slots }) => {
|
||||
const data = shallowReactive({ ...props });
|
||||
data.padding ??= 8;
|
||||
data.width ??= 200;
|
||||
data.height ??= 200;
|
||||
data.id ??= '';
|
||||
|
||||
const store = TextboxStore.use(props.id ?? getNextTextboxId(), data);
|
||||
const hidden = ref(false);
|
||||
store.on('hide', () => (hidden.value = true));
|
||||
store.on('show', () => (hidden.value = false));
|
||||
onUpdated(() => {
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
// @ts-ignore
|
||||
if (!isNil(value)) data[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
const contentWidth = computed(() => data.width! - data.padding! * 2);
|
||||
const contentHeight = computed(() => data.height! - data.padding! * 2);
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<container>
|
||||
<container hidden={hidden.value} id="11111">
|
||||
{slots.default ? (
|
||||
slots.default()
|
||||
slots.default(data)
|
||||
) : props.winskin ? (
|
||||
// todo
|
||||
<winskin></winskin>
|
||||
@ -473,19 +522,23 @@ export const Textbox = defineComponent<TextboxProps, {}, string, TextboxSlots>(
|
||||
<g-rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={props.width ?? 200}
|
||||
height={props.height ?? 200}
|
||||
width={data.width ?? 200}
|
||||
height={data.height ?? 200}
|
||||
fill
|
||||
fillStyle={props.backColor}
|
||||
fillStyle={data.backColor}
|
||||
></g-rect>
|
||||
)}
|
||||
<TextContent {...props}></TextContent>
|
||||
<TextContent
|
||||
{...data}
|
||||
x={data.padding}
|
||||
y={data.padding}
|
||||
width={contentWidth.value}
|
||||
height={contentHeight.value}
|
||||
></TextContent>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
},
|
||||
textboxOptions
|
||||
);
|
||||
}, textboxOptions);
|
||||
|
||||
const fontSizeGuessScale = new Map<string, number>([
|
||||
['px', 1],
|
||||
@ -531,6 +584,7 @@ function splitLines(data: TextContentData) {
|
||||
let mid = 0;
|
||||
let guessCount = 1;
|
||||
let splitProgress = false;
|
||||
let last = 0;
|
||||
|
||||
while (1) {
|
||||
if (!splitProgress) {
|
||||
@ -548,8 +602,15 @@ function splitLines(data: TextContentData) {
|
||||
const diff = end - start;
|
||||
|
||||
if (diff === 1) {
|
||||
if (start === last) {
|
||||
res.push(words[last + 1]);
|
||||
start = words[last + 1];
|
||||
end = start + 1;
|
||||
} else {
|
||||
res.push(words[start]);
|
||||
}
|
||||
if (end >= words.length) break;
|
||||
last = resolved;
|
||||
resolved = start;
|
||||
end = Math.ceil(start + guess);
|
||||
if (end > words.length) end = words.length;
|
||||
@ -659,3 +720,67 @@ function testHeight(text: string, font: string) {
|
||||
ctx.font = font;
|
||||
return ctx.measureText(text).fontBoundingBoxAscent;
|
||||
}
|
||||
|
||||
interface TextboxStoreEvent {
|
||||
update: [value: TextboxProps];
|
||||
show: [];
|
||||
hide: [];
|
||||
}
|
||||
|
||||
export class TextboxStore extends EventEmitter<TextboxStoreEvent> {
|
||||
static list: Map<string, TextboxStore> = new Map();
|
||||
|
||||
private constructor(private readonly data: TextboxProps) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改渲染数据
|
||||
*/
|
||||
modify(data: Partial<TextboxProps>) {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// @ts-ignore
|
||||
if (!isNil(value)) this.data[key] = value;
|
||||
}
|
||||
this.emit('update', this.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示文本框
|
||||
*/
|
||||
show() {
|
||||
this.emit('show');
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏文本框
|
||||
*/
|
||||
hide() {
|
||||
this.emit('hide');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本框
|
||||
* @param id 文本框id
|
||||
*/
|
||||
static get(id: string): TextboxStore | undefined {
|
||||
return this.list.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前作用域下生成文本框控制器
|
||||
* @param id 文本框id
|
||||
* @param props 文本框渲染数据
|
||||
*/
|
||||
static use(id: string, props: TextboxProps) {
|
||||
const store = new TextboxStore(props);
|
||||
if (this.list.has(id)) {
|
||||
logger.warn(42, id);
|
||||
}
|
||||
this.list.set(id, store);
|
||||
onUnmounted(() => {
|
||||
this.list.delete(id);
|
||||
});
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
@ -159,6 +159,7 @@ const beforeFrame: (() => void)[] = [];
|
||||
const afterFrame: (() => void)[] = [];
|
||||
const renderFrame: (() => void)[] = [];
|
||||
|
||||
let count = 0;
|
||||
export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
extends EventEmitter<ERenderItemEvent | E>
|
||||
implements
|
||||
@ -182,6 +183,8 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
/** id到渲染元素的映射 */
|
||||
static itemMap: Map<string, RenderItem> = new Map();
|
||||
|
||||
readonly uid: number = count++;
|
||||
|
||||
private _id: string = '';
|
||||
|
||||
get id(): string {
|
||||
@ -399,11 +402,11 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
this.anchorY = y;
|
||||
}
|
||||
|
||||
update(item: RenderItem<any> = this): void {
|
||||
if (this.needUpdate || this.hidden) return;
|
||||
update(item: RenderItem<any> = this, force: boolean = false): void {
|
||||
if ((this.needUpdate || this.hidden) && !force) return;
|
||||
this.needUpdate = true;
|
||||
this.cacheDirty = true;
|
||||
this.parent?.update(item);
|
||||
this.parent?.update(item, force);
|
||||
}
|
||||
|
||||
setHD(hd: boolean): void {
|
||||
@ -473,7 +476,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
hide() {
|
||||
if (this.hidden) return;
|
||||
this.hidden = true;
|
||||
this.update(this);
|
||||
this.update(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -482,7 +485,23 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
show() {
|
||||
if (!this.hidden) return;
|
||||
this.hidden = false;
|
||||
this.update(this);
|
||||
this.refreshAllChildren(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有子元素
|
||||
*/
|
||||
refreshAllChildren(force: boolean = false) {
|
||||
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, force);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -555,7 +574,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
if (typeof expected === 'string') {
|
||||
const type = typeof value;
|
||||
if (type !== expected) {
|
||||
logger.warn(21, key, expected, type);
|
||||
logger.error(21, key, expected, type);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@ -564,7 +583,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
if (value instanceof expected) {
|
||||
return true;
|
||||
} else {
|
||||
logger.warn(
|
||||
logger.error(
|
||||
21,
|
||||
key,
|
||||
expected.name,
|
||||
|
@ -9,7 +9,6 @@ export class MotaRenderer extends Container {
|
||||
|
||||
target!: MotaOffscreenCanvas2D;
|
||||
|
||||
protected needUpdate: boolean = false;
|
||||
readonly isRoot: boolean = true;
|
||||
|
||||
constructor(id: string = 'render-main') {
|
||||
@ -84,27 +83,13 @@ export class MotaRenderer extends Container {
|
||||
this.target.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有元素
|
||||
*/
|
||||
refreshAll() {
|
||||
const stack: RenderItem[] = [this];
|
||||
while (stack.length > 0) {
|
||||
const item = stack.pop();
|
||||
if (!item) break;
|
||||
if (item.children.size === 0) {
|
||||
item.update();
|
||||
} else {
|
||||
item.children.forEach(v => stack.push(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get(id: string) {
|
||||
return this.list.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
MotaRenderer.list.forEach(v => v.requestAfterFrame(() => v.refreshAll()));
|
||||
MotaRenderer.list.forEach(v =>
|
||||
v.requestAfterFrame(() => v.refreshAllChildren())
|
||||
);
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
|
||||
},
|
||||
|
||||
createText: function (text: string): RenderItem<ERenderItemEvent> {
|
||||
logger.warn(38);
|
||||
if (!/^\s*$/.test(text)) logger.warn(38);
|
||||
return new Text(text);
|
||||
},
|
||||
|
||||
|
@ -69,6 +69,7 @@
|
||||
"39": "Plain text is not supported outside Text element.",
|
||||
"40": "Cannot return canvas that is not provided by this pool.",
|
||||
"41": "Width of text content components must be positive. receive: $1",
|
||||
"42": "Repeat Textbox id: '$1'.",
|
||||
"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."
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user