mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 04:19:30 +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 { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||||
import {
|
import {
|
||||||
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
onUpdated,
|
onUpdated,
|
||||||
ref,
|
ref,
|
||||||
|
shallowReactive,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
SlotsType,
|
SlotsType,
|
||||||
VNode,
|
VNode,
|
||||||
@ -16,6 +19,8 @@ import { Sprite } from '../sprite';
|
|||||||
import { onTick } from '../renderer';
|
import { onTick } from '../renderer';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { SetupComponentOptions } from './types';
|
import { SetupComponentOptions } from './types';
|
||||||
|
import EventEmitter from 'eventemitter3';
|
||||||
|
import { Container } from '../container';
|
||||||
|
|
||||||
export const enum WordBreak {
|
export const enum WordBreak {
|
||||||
/** 不换行 */
|
/** 不换行 */
|
||||||
@ -128,364 +133,412 @@ const textContentOptions = {
|
|||||||
'width',
|
'width',
|
||||||
'wordBreak',
|
'wordBreak',
|
||||||
'x',
|
'x',
|
||||||
'y'
|
'y',
|
||||||
|
'fill',
|
||||||
|
'fillStyle',
|
||||||
|
'strokeStyle',
|
||||||
|
'strokeWidth',
|
||||||
|
'stroke'
|
||||||
],
|
],
|
||||||
emits: ['typeEnd', 'typeStart']
|
emits: ['typeEnd', 'typeStart']
|
||||||
} satisfies SetupComponentOptions<TextContentProps, TextContentEmits>;
|
} satisfies SetupComponentOptions<
|
||||||
|
TextContentProps,
|
||||||
|
TextContentEmits,
|
||||||
|
keyof TextContentEmits
|
||||||
|
>;
|
||||||
|
|
||||||
export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
export const TextContent = defineComponent<
|
||||||
(props, { emit }) => {
|
TextContentProps,
|
||||||
if (props.width && props.width <= 0) {
|
TextContentEmits,
|
||||||
logger.warn(41, String(props.width));
|
keyof TextContentEmits
|
||||||
}
|
>((props, { emit }) => {
|
||||||
const renderData: Required<TextContentProps> = {
|
if (props.width && props.width <= 0) {
|
||||||
text: props.text,
|
logger.warn(41, String(props.width));
|
||||||
textAlign: props.textAlign ?? TextAlign.Left,
|
}
|
||||||
x: props.x ?? 0,
|
const renderData: Required<TextContentProps> = {
|
||||||
y: props.y ?? 0,
|
text: props.text,
|
||||||
width: !props.width || props.width <= 0 ? 200 : props.width,
|
textAlign: props.textAlign ?? TextAlign.Left,
|
||||||
height: props.height ?? 200,
|
x: props.x ?? 0,
|
||||||
font:
|
y: props.y ?? 0,
|
||||||
props.font ??
|
width: !props.width || props.width <= 0 ? 200 : props.width,
|
||||||
core.status.globalAttribute?.font ??
|
height: props.height ?? 200,
|
||||||
'16px Verdana',
|
font: props.font ?? core.status.globalAttribute?.font ?? '16px Verdana',
|
||||||
ignoreLineEnd: props.ignoreLineEnd ?? new Set(),
|
ignoreLineEnd: props.ignoreLineEnd ?? new Set(),
|
||||||
ignoreLineStart: props.ignoreLineStart ?? new Set(),
|
ignoreLineStart: props.ignoreLineStart ?? new Set(),
|
||||||
keepLast: props.keepLast ?? false,
|
keepLast: props.keepLast ?? false,
|
||||||
interval: props.interval ?? 0,
|
interval: props.interval ?? 0,
|
||||||
lineHeight: props.lineHeight ?? 0,
|
lineHeight: props.lineHeight ?? 0,
|
||||||
wordBreak: props.wordBreak ?? WordBreak.Space,
|
wordBreak: props.wordBreak ?? WordBreak.Space,
|
||||||
breakChars: props.breakChars ?? new Set(),
|
breakChars: props.breakChars ?? new Set(),
|
||||||
fillStyle: props.fillStyle ?? '#fff',
|
fillStyle: props.fillStyle ?? '#fff',
|
||||||
strokeStyle: props.strokeStyle ?? 'transparent',
|
strokeStyle: props.strokeStyle ?? 'transparent',
|
||||||
fill: props.fill ?? true,
|
fill: props.fill ?? true,
|
||||||
stroke: props.stroke ?? false,
|
stroke: props.stroke ?? false,
|
||||||
strokeWidth: props.strokeWidth ?? 2
|
strokeWidth: props.strokeWidth ?? 2
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureProps = () => {
|
const ensureProps = () => {
|
||||||
for (const [key, value] of Object.entries(props)) {
|
for (const [key, value] of Object.entries(props)) {
|
||||||
if (key in renderData && !isNil(value)) {
|
if (key in renderData && !isNil(value)) {
|
||||||
if (key === 'width') {
|
if (key === 'width') {
|
||||||
if (value && value <= 0) {
|
if (value && value <= 0) {
|
||||||
logger.warn(41, String(props.width));
|
logger.warn(41, String(props.width));
|
||||||
renderData.width = 200;
|
renderData.width = 200;
|
||||||
} else {
|
|
||||||
renderData.width = value;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
renderData.width = value;
|
||||||
renderData[key] = value;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeSplitData = (): TextContentData => {
|
|
||||||
ensureProps();
|
|
||||||
return {
|
|
||||||
text: renderData.text,
|
|
||||||
width: renderData.width!,
|
|
||||||
font: renderData.font!,
|
|
||||||
wordBreak: renderData.wordBreak!,
|
|
||||||
ignoreLineStart: new Set(renderData.ignoreLineStart),
|
|
||||||
ignoreLineEnd: new Set(renderData.ignoreLineEnd),
|
|
||||||
breakChars: new Set(renderData.breakChars)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否需要重新分行
|
|
||||||
*/
|
|
||||||
const needResplit = (value: TextContentData, old: TextContentData) => {
|
|
||||||
return (
|
|
||||||
value.text !== old.text ||
|
|
||||||
value.font !== old.font ||
|
|
||||||
value.width !== old.width ||
|
|
||||||
value.wordBreak !== old.wordBreak ||
|
|
||||||
!isSetEqual(value.breakChars, old.breakChars) ||
|
|
||||||
!isSetEqual(value.ignoreLineEnd, old.ignoreLineEnd) ||
|
|
||||||
!isSetEqual(value.ignoreLineStart, old.ignoreLineStart)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 每行的渲染信息 */
|
|
||||||
const renderable: TextContentRenderable[] = [];
|
|
||||||
/** 需要更新的行数 */
|
|
||||||
const dirtyIndex: number[] = [];
|
|
||||||
const spriteElement = ref<Sprite>();
|
|
||||||
|
|
||||||
/** dirtyIndex 更新指针 */
|
|
||||||
let linePointer = 0;
|
|
||||||
let startTime = 0;
|
|
||||||
/** 从哪个字符开始渲染 */
|
|
||||||
let fromChar = 0;
|
|
||||||
/** 是否需要更新渲染 */
|
|
||||||
let needUpdate = false;
|
|
||||||
const tick = () => {
|
|
||||||
if (!needUpdate) return;
|
|
||||||
spriteElement.value?.update();
|
|
||||||
const time = Date.now();
|
|
||||||
const char =
|
|
||||||
Math.floor((time - startTime) / renderData.interval!) +
|
|
||||||
fromChar;
|
|
||||||
if (!isFinite(char)) {
|
|
||||||
renderable.forEach(v => (v.pointer = v.text.length));
|
|
||||||
needUpdate = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
while (linePointer < dirtyIndex.length) {
|
|
||||||
const line = dirtyIndex[linePointer];
|
|
||||||
const data = renderable[line];
|
|
||||||
const pointer = char - data.from;
|
|
||||||
if (char >= data.to) {
|
|
||||||
data.pointer = data.text.length;
|
|
||||||
linePointer++;
|
|
||||||
} else {
|
} else {
|
||||||
data.pointer = pointer;
|
// @ts-ignore
|
||||||
break;
|
renderData[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (linePointer >= dirtyIndex.length) {
|
}
|
||||||
needUpdate = false;
|
};
|
||||||
renderable.forEach(v => (v.pointer = v.text.length));
|
|
||||||
emit('typeEnd');
|
const makeSplitData = (): TextContentData => {
|
||||||
}
|
ensureProps();
|
||||||
|
return {
|
||||||
|
text: renderData.text,
|
||||||
|
width: renderData.width!,
|
||||||
|
font: renderData.font!,
|
||||||
|
wordBreak: renderData.wordBreak!,
|
||||||
|
ignoreLineStart: new Set(renderData.ignoreLineStart),
|
||||||
|
ignoreLineEnd: new Set(renderData.ignoreLineEnd),
|
||||||
|
breakChars: new Set(renderData.breakChars)
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
onTick(tick);
|
/**
|
||||||
onMounted(() => {
|
* 判断是否需要重新分行
|
||||||
data.value = makeSplitData();
|
*/
|
||||||
lineData.value = splitLines(data.value);
|
const needResplit = (value: TextContentData, old: TextContentData) => {
|
||||||
});
|
return (
|
||||||
|
value.text !== old.text ||
|
||||||
|
value.font !== old.font ||
|
||||||
|
value.width !== old.width ||
|
||||||
|
value.wordBreak !== old.wordBreak ||
|
||||||
|
!isSetEqual(value.breakChars, old.breakChars) ||
|
||||||
|
!isSetEqual(value.ignoreLineEnd, old.ignoreLineEnd) ||
|
||||||
|
!isSetEqual(value.ignoreLineStart, old.ignoreLineStart)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderContent = (
|
/** 每行的渲染信息 */
|
||||||
canvas: MotaOffscreenCanvas2D,
|
const renderable: TextContentRenderable[] = [];
|
||||||
transform: Transform
|
/** 需要更新的行数 */
|
||||||
) => {
|
const dirtyIndex: number[] = [];
|
||||||
const ctx = canvas.ctx;
|
const spriteElement = ref<Sprite>();
|
||||||
ctx.font = renderData.font;
|
|
||||||
ctx.fillStyle = renderData.fillStyle;
|
|
||||||
ctx.strokeStyle = renderData.strokeStyle;
|
|
||||||
ctx.lineWidth = renderData.strokeWidth;
|
|
||||||
|
|
||||||
renderable.forEach(v => {
|
/** dirtyIndex 更新指针 */
|
||||||
if (v.pointer === 0) return;
|
let linePointer = 0;
|
||||||
const text = v.text.slice(0, v.pointer);
|
let startTime = 0;
|
||||||
|
/** 从哪个字符开始渲染 */
|
||||||
|
let fromChar = 0;
|
||||||
|
/** 是否需要更新渲染 */
|
||||||
|
let needUpdate = false;
|
||||||
|
const tick = () => {
|
||||||
|
if (!needUpdate) return;
|
||||||
|
spriteElement.value?.update();
|
||||||
|
const time = Date.now();
|
||||||
|
const char =
|
||||||
|
Math.floor((time - startTime) / renderData.interval!) + fromChar;
|
||||||
|
if (!isFinite(char)) {
|
||||||
|
renderable.forEach(v => (v.pointer = v.text.length));
|
||||||
|
needUpdate = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (linePointer < dirtyIndex.length) {
|
||||||
|
const line = dirtyIndex[linePointer];
|
||||||
|
const data = renderable[line];
|
||||||
|
const pointer = char - data.from;
|
||||||
|
if (char >= data.to) {
|
||||||
|
data.pointer = data.text.length;
|
||||||
|
linePointer++;
|
||||||
|
} else {
|
||||||
|
data.pointer = pointer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (linePointer >= dirtyIndex.length) {
|
||||||
|
needUpdate = false;
|
||||||
|
renderable.forEach(v => (v.pointer = v.text.length));
|
||||||
|
emit('typeEnd');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onTick(tick);
|
||||||
|
onMounted(() => {
|
||||||
|
data.value = makeSplitData();
|
||||||
|
lineData.value = splitLines(data.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderContent = (
|
||||||
|
canvas: MotaOffscreenCanvas2D,
|
||||||
|
transform: Transform
|
||||||
|
) => {
|
||||||
|
const ctx = canvas.ctx;
|
||||||
|
ctx.font = renderData.font;
|
||||||
|
ctx.fillStyle = renderData.fillStyle;
|
||||||
|
ctx.strokeStyle = renderData.strokeStyle;
|
||||||
|
ctx.lineWidth = renderData.strokeWidth;
|
||||||
|
|
||||||
|
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.stroke) ctx.strokeText(text, v.x, v.y);
|
||||||
if (renderData.fill) ctx.fillText(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 renderable 对象
|
* 生成 renderable 对象
|
||||||
* @param text 全部文本
|
* @param text 全部文本
|
||||||
* @param lines 分行信息
|
* @param lines 分行信息
|
||||||
* @param index 从第几行开始生成
|
* @param index 从第几行开始生成
|
||||||
* @param from 从第几个字符开始生成
|
* @param from 从第几个字符开始生成
|
||||||
*/
|
*/
|
||||||
const makeRenderable = (
|
const makeRenderable = (
|
||||||
text: string,
|
text: string,
|
||||||
lines: number[],
|
lines: number[],
|
||||||
index: number,
|
index: number,
|
||||||
from: number
|
from: number
|
||||||
) => {
|
) => {
|
||||||
renderable.splice(index);
|
renderable.splice(index);
|
||||||
dirtyIndex.splice(0);
|
dirtyIndex.splice(0);
|
||||||
dirtyIndex.push(index);
|
dirtyIndex.push(index);
|
||||||
// 初始化渲染
|
// 初始化渲染
|
||||||
linePointer = 0;
|
linePointer = 0;
|
||||||
startTime = Date.now();
|
startTime = Date.now();
|
||||||
fromChar = from;
|
fromChar = from;
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
|
|
||||||
let startY = renderable.reduce(
|
let startY = renderable.reduce(
|
||||||
(prev, curr) => prev + curr.textHeight + curr.height,
|
(prev, curr) => prev + curr.textHeight + curr.height,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
// 第一个比较特殊,需要特判
|
// 第一个比较特殊,需要特判
|
||||||
const start = lines[index - 1] ?? 0;
|
const start = lines[index - 1] ?? 0;
|
||||||
const end = lines[index];
|
const end = lines[index];
|
||||||
const startPointer = from > start && from < end ? from - start : 0;
|
const startPointer = from > start && from < end ? from - start : 0;
|
||||||
|
const height = testHeight(text, renderData.font!);
|
||||||
|
startY += height;
|
||||||
|
renderable.push({
|
||||||
|
text: text.slice(start, end),
|
||||||
|
x: 0,
|
||||||
|
y: startY,
|
||||||
|
height: renderData.lineHeight!,
|
||||||
|
textHeight: height,
|
||||||
|
pointer: startPointer,
|
||||||
|
from: start,
|
||||||
|
to: end
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = index + 1; i < lines.length; i++) {
|
||||||
|
dirtyIndex.push(i);
|
||||||
|
const start = lines[i - 1] ?? 0;
|
||||||
|
const end = lines[i];
|
||||||
const height = testHeight(text, renderData.font!);
|
const height = testHeight(text, renderData.font!);
|
||||||
startY += height;
|
startY += height;
|
||||||
|
|
||||||
renderable.push({
|
renderable.push({
|
||||||
text: text.slice(start, end),
|
text: text.slice(start, end),
|
||||||
x: 0,
|
x: 0,
|
||||||
y: startY,
|
y: startY,
|
||||||
height: renderData.lineHeight!,
|
height: renderData.lineHeight!,
|
||||||
textHeight: height,
|
textHeight: height,
|
||||||
pointer: startPointer,
|
pointer: 0,
|
||||||
from: start,
|
from: start,
|
||||||
to: end
|
to: end
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
emit('typeStart');
|
||||||
|
};
|
||||||
|
|
||||||
for (let i = index + 1; i < lines.length; i++) {
|
/**
|
||||||
dirtyIndex.push(i);
|
* 从头开始渲染
|
||||||
const start = lines[i - 1] ?? 0;
|
*/
|
||||||
const end = lines[i];
|
const rawRender = (text: string, lines: number[]) => {
|
||||||
const height = testHeight(text, renderData.font!);
|
makeRenderable(text, lines, 0, 0);
|
||||||
startY += height;
|
spriteElement.value?.update();
|
||||||
|
};
|
||||||
|
|
||||||
renderable.push({
|
/**
|
||||||
text: text.slice(start, end),
|
* 接续上一个继续渲染
|
||||||
x: 0,
|
* @param from 从第几个字符接续渲染
|
||||||
y: startY,
|
* @param lines 分行信息
|
||||||
height: renderData.lineHeight!,
|
* @param index 从第几行接续渲染
|
||||||
textHeight: height,
|
*/
|
||||||
pointer: 0,
|
const continueRender = (
|
||||||
from: start,
|
text: string,
|
||||||
to: end
|
from: number,
|
||||||
});
|
lines: number[],
|
||||||
}
|
index: number
|
||||||
emit('typeStart');
|
) => {
|
||||||
};
|
makeRenderable(text, lines, index, from);
|
||||||
|
spriteElement.value?.update();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
const data = shallowRef<TextContentData>(makeSplitData());
|
||||||
* 从头开始渲染
|
|
||||||
*/
|
|
||||||
const rawRender = (text: string, lines: number[]) => {
|
|
||||||
makeRenderable(text, lines, 0, 0);
|
|
||||||
spriteElement.value?.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
onUpdated(() => {
|
||||||
* 接续上一个继续渲染
|
data.value = makeSplitData();
|
||||||
* @param from 从第几个字符接续渲染
|
});
|
||||||
* @param lines 分行信息
|
|
||||||
* @param index 从第几行接续渲染
|
|
||||||
*/
|
|
||||||
const continueRender = (
|
|
||||||
text: string,
|
|
||||||
from: number,
|
|
||||||
lines: number[],
|
|
||||||
index: number
|
|
||||||
) => {
|
|
||||||
makeRenderable(text, lines, index, from);
|
|
||||||
spriteElement.value?.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = shallowRef<TextContentData>(makeSplitData());
|
let shouldKeep = false;
|
||||||
|
const lineData = shallowRef([0]);
|
||||||
|
watch(data, (value, old) => {
|
||||||
|
if (needResplit(value, old)) {
|
||||||
|
lineData.value = splitLines(value);
|
||||||
|
}
|
||||||
|
|
||||||
onUpdated(() => {
|
if (renderData.keepLast && value.text.startsWith(old.text)) {
|
||||||
data.value = makeSplitData();
|
shouldKeep = true;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let shouldKeep = false;
|
// 判断是否需要接续渲染
|
||||||
const lineData = shallowRef([0]);
|
watch(lineData, (value, old) => {
|
||||||
watch(data, (value, old) => {
|
if (shouldKeep) {
|
||||||
if (needResplit(value, old)) {
|
shouldKeep = false;
|
||||||
lineData.value = splitLines(value);
|
const isSub = old.slice(0, -1).every((v, i) => v === value[i]);
|
||||||
}
|
|
||||||
|
|
||||||
if (renderData.keepLast && value.text.startsWith(old.text)) {
|
// 有点地狱的条件分歧,大体就是分为两种情况,一种是两个末尾一致,如果一致那直接从下一行接着画就完事了
|
||||||
shouldKeep = true;
|
// 但是如果不一致,那么从旧的最后一个开始往后画
|
||||||
}
|
if (isSub) {
|
||||||
});
|
const last = value[old.length - 1];
|
||||||
|
const oldLast = value.at(-1);
|
||||||
// 判断是否需要接续渲染
|
if (!last) {
|
||||||
watch(lineData, (value, old) => {
|
|
||||||
if (shouldKeep) {
|
|
||||||
shouldKeep = false;
|
|
||||||
const isSub = old.slice(0, -1).every((v, i) => v === value[i]);
|
|
||||||
|
|
||||||
// 有点地狱的条件分歧,大体就是分为两种情况,一种是两个末尾一致,如果一致那直接从下一行接着画就完事了
|
|
||||||
// 但是如果不一致,那么从旧的最后一个开始往后画
|
|
||||||
if (isSub) {
|
|
||||||
const last = value[old.length - 1];
|
|
||||||
const oldLast = value.at(-1);
|
|
||||||
if (!last) {
|
|
||||||
rawRender(data.value.text, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (last === oldLast) {
|
|
||||||
const index = old.length - 1;
|
|
||||||
continueRender(data.value.text, last, value, index);
|
|
||||||
} else {
|
|
||||||
if (!oldLast) {
|
|
||||||
rawRender(data.value.text, value);
|
|
||||||
} else {
|
|
||||||
const index = old.length - 1;
|
|
||||||
continueRender(
|
|
||||||
data.value.text,
|
|
||||||
oldLast,
|
|
||||||
value,
|
|
||||||
index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rawRender(data.value.text, value);
|
rawRender(data.value.text, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (last === oldLast) {
|
||||||
|
const index = old.length - 1;
|
||||||
|
continueRender(data.value.text, last, value, index);
|
||||||
|
} else {
|
||||||
|
if (!oldLast) {
|
||||||
|
rawRender(data.value.text, value);
|
||||||
|
} else {
|
||||||
|
const index = old.length - 1;
|
||||||
|
continueRender(data.value.text, oldLast, value, index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rawRender(data.value.text, value);
|
rawRender(data.value.text, value);
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
rawRender(data.value.text, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return (
|
return (
|
||||||
<sprite
|
<sprite
|
||||||
ref={spriteElement}
|
ref={spriteElement}
|
||||||
hd
|
hd
|
||||||
antiAliasing={false}
|
antiAliasing={true}
|
||||||
x={renderData.x}
|
x={renderData.x}
|
||||||
y={renderData.y}
|
y={renderData.y}
|
||||||
width={renderData.width}
|
width={renderData.width}
|
||||||
height={renderData.height}
|
height={renderData.height}
|
||||||
render={renderContent}
|
render={renderContent}
|
||||||
></sprite>
|
></sprite>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
}, textContentOptions);
|
||||||
textContentOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface TextboxProps extends TextContentProps {
|
export interface TextboxProps extends TextContentProps {
|
||||||
|
id?: string;
|
||||||
/** 背景颜色 */
|
/** 背景颜色 */
|
||||||
backColor?: CanvasStyle;
|
backColor?: CanvasStyle;
|
||||||
/** 背景 winskin */
|
/** 背景 winskin */
|
||||||
winskin?: string;
|
winskin?: string;
|
||||||
|
/** 边框与文字间的距离,默认为8 */
|
||||||
|
padding?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextboxSlots extends SlotsType {
|
type TextboxEmits = TextContentEmits;
|
||||||
default: () => VNode;
|
type TextboxSlots = SlotsType<{ default: (data: TextboxProps) => VNode[] }>;
|
||||||
}
|
|
||||||
|
|
||||||
const textboxOptions = {
|
const textboxOptions = {
|
||||||
props: (textContentOptions.props as (keyof TextboxProps)[]).concat([
|
props: (textContentOptions.props as (keyof TextboxProps)[]).concat([
|
||||||
'backColor',
|
'backColor',
|
||||||
'winskin'
|
'winskin',
|
||||||
])
|
'id'
|
||||||
|
]),
|
||||||
|
emits: textContentOptions.emits
|
||||||
} satisfies SetupComponentOptions<TextboxProps, {}, string, TextboxSlots>;
|
} satisfies SetupComponentOptions<TextboxProps, {}, string, TextboxSlots>;
|
||||||
|
|
||||||
export const Textbox = defineComponent<TextboxProps, {}, string, TextboxSlots>(
|
let id = 0;
|
||||||
(props, { slots }) => {
|
function getNextTextboxId() {
|
||||||
return () => {
|
return `@default-textbox-${id++}`;
|
||||||
return (
|
}
|
||||||
<container>
|
|
||||||
{slots.default ? (
|
export const Textbox = defineComponent<
|
||||||
slots.default()
|
TextboxProps,
|
||||||
) : props.winskin ? (
|
TextboxEmits,
|
||||||
// todo
|
keyof TextboxEmits,
|
||||||
<winskin></winskin>
|
TextboxSlots
|
||||||
) : (
|
>((props, { slots }) => {
|
||||||
// todo
|
const data = shallowReactive({ ...props });
|
||||||
<g-rect
|
data.padding ??= 8;
|
||||||
x={0}
|
data.width ??= 200;
|
||||||
y={0}
|
data.height ??= 200;
|
||||||
width={props.width ?? 200}
|
data.id ??= '';
|
||||||
height={props.height ?? 200}
|
|
||||||
fill
|
const store = TextboxStore.use(props.id ?? getNextTextboxId(), data);
|
||||||
fillStyle={props.backColor}
|
const hidden = ref(false);
|
||||||
></g-rect>
|
store.on('hide', () => (hidden.value = true));
|
||||||
)}
|
store.on('show', () => (hidden.value = false));
|
||||||
<TextContent {...props}></TextContent>
|
onUpdated(() => {
|
||||||
</container>
|
for (const [key, value] of Object.entries(props)) {
|
||||||
);
|
// @ts-ignore
|
||||||
};
|
if (!isNil(value)) data[key] = value;
|
||||||
},
|
}
|
||||||
textboxOptions
|
});
|
||||||
);
|
|
||||||
|
const contentWidth = computed(() => data.width! - data.padding! * 2);
|
||||||
|
const contentHeight = computed(() => data.height! - data.padding! * 2);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return (
|
||||||
|
<container hidden={hidden.value} id="11111">
|
||||||
|
{slots.default ? (
|
||||||
|
slots.default(data)
|
||||||
|
) : props.winskin ? (
|
||||||
|
// todo
|
||||||
|
<winskin></winskin>
|
||||||
|
) : (
|
||||||
|
// todo
|
||||||
|
<g-rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={data.width ?? 200}
|
||||||
|
height={data.height ?? 200}
|
||||||
|
fill
|
||||||
|
fillStyle={data.backColor}
|
||||||
|
></g-rect>
|
||||||
|
)}
|
||||||
|
<TextContent
|
||||||
|
{...data}
|
||||||
|
x={data.padding}
|
||||||
|
y={data.padding}
|
||||||
|
width={contentWidth.value}
|
||||||
|
height={contentHeight.value}
|
||||||
|
></TextContent>
|
||||||
|
</container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, textboxOptions);
|
||||||
|
|
||||||
const fontSizeGuessScale = new Map<string, number>([
|
const fontSizeGuessScale = new Map<string, number>([
|
||||||
['px', 1],
|
['px', 1],
|
||||||
@ -531,6 +584,7 @@ function splitLines(data: TextContentData) {
|
|||||||
let mid = 0;
|
let mid = 0;
|
||||||
let guessCount = 1;
|
let guessCount = 1;
|
||||||
let splitProgress = false;
|
let splitProgress = false;
|
||||||
|
let last = 0;
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
if (!splitProgress) {
|
if (!splitProgress) {
|
||||||
@ -548,8 +602,15 @@ function splitLines(data: TextContentData) {
|
|||||||
const diff = end - start;
|
const diff = end - start;
|
||||||
|
|
||||||
if (diff === 1) {
|
if (diff === 1) {
|
||||||
res.push(words[start]);
|
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;
|
if (end >= words.length) break;
|
||||||
|
last = resolved;
|
||||||
resolved = start;
|
resolved = start;
|
||||||
end = Math.ceil(start + guess);
|
end = Math.ceil(start + guess);
|
||||||
if (end > words.length) end = words.length;
|
if (end > words.length) end = words.length;
|
||||||
@ -659,3 +720,67 @@ function testHeight(text: string, font: string) {
|
|||||||
ctx.font = font;
|
ctx.font = font;
|
||||||
return ctx.measureText(text).fontBoundingBoxAscent;
|
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 afterFrame: (() => void)[] = [];
|
||||||
const renderFrame: (() => void)[] = [];
|
const renderFrame: (() => void)[] = [];
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||||
extends EventEmitter<ERenderItemEvent | E>
|
extends EventEmitter<ERenderItemEvent | E>
|
||||||
implements
|
implements
|
||||||
@ -182,6 +183,8 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
|||||||
/** id到渲染元素的映射 */
|
/** id到渲染元素的映射 */
|
||||||
static itemMap: Map<string, RenderItem> = new Map();
|
static itemMap: Map<string, RenderItem> = new Map();
|
||||||
|
|
||||||
|
readonly uid: number = count++;
|
||||||
|
|
||||||
private _id: string = '';
|
private _id: string = '';
|
||||||
|
|
||||||
get id(): string {
|
get id(): string {
|
||||||
@ -399,11 +402,11 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
|||||||
this.anchorY = y;
|
this.anchorY = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(item: RenderItem<any> = this): void {
|
update(item: RenderItem<any> = this, force: boolean = false): void {
|
||||||
if (this.needUpdate || this.hidden) return;
|
if ((this.needUpdate || this.hidden) && !force) return;
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
this.cacheDirty = true;
|
this.cacheDirty = true;
|
||||||
this.parent?.update(item);
|
this.parent?.update(item, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHD(hd: boolean): void {
|
setHD(hd: boolean): void {
|
||||||
@ -473,7 +476,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
|||||||
hide() {
|
hide() {
|
||||||
if (this.hidden) return;
|
if (this.hidden) return;
|
||||||
this.hidden = true;
|
this.hidden = true;
|
||||||
this.update(this);
|
this.update(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -482,7 +485,23 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
|||||||
show() {
|
show() {
|
||||||
if (!this.hidden) return;
|
if (!this.hidden) return;
|
||||||
this.hidden = false;
|
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') {
|
if (typeof expected === 'string') {
|
||||||
const type = typeof value;
|
const type = typeof value;
|
||||||
if (type !== expected) {
|
if (type !== expected) {
|
||||||
logger.warn(21, key, expected, type);
|
logger.error(21, key, expected, type);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
@ -564,7 +583,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
|||||||
if (value instanceof expected) {
|
if (value instanceof expected) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.error(
|
||||||
21,
|
21,
|
||||||
key,
|
key,
|
||||||
expected.name,
|
expected.name,
|
||||||
|
@ -9,7 +9,6 @@ export class MotaRenderer extends Container {
|
|||||||
|
|
||||||
target!: MotaOffscreenCanvas2D;
|
target!: MotaOffscreenCanvas2D;
|
||||||
|
|
||||||
protected needUpdate: boolean = false;
|
|
||||||
readonly isRoot: boolean = true;
|
readonly isRoot: boolean = true;
|
||||||
|
|
||||||
constructor(id: string = 'render-main') {
|
constructor(id: string = 'render-main') {
|
||||||
@ -84,27 +83,13 @@ export class MotaRenderer extends Container {
|
|||||||
this.target.delete();
|
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) {
|
static get(id: string) {
|
||||||
return this.list.get(id);
|
return this.list.get(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
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> {
|
createText: function (text: string): RenderItem<ERenderItemEvent> {
|
||||||
logger.warn(38);
|
if (!/^\s*$/.test(text)) logger.warn(38);
|
||||||
return new Text(text);
|
return new Text(text);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@
|
|||||||
"39": "Plain text is not supported outside Text element.",
|
"39": "Plain text is not supported outside Text element.",
|
||||||
"40": "Cannot return canvas that is not provided by this pool.",
|
"40": "Cannot return canvas that is not provided by this pool.",
|
||||||
"41": "Width of text content components must be positive. receive: $1",
|
"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.",
|
"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."
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user