mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-25 00:23:25 +08:00
Compare commits
No commits in common. "b018a5fd9a82c80eec3e6d9340f125c2a5a46c50" and "c0e05a9817415d5178a39324c05d56e0284539c8" have entirely different histories.
b018a5fd9a
...
c0e05a9817
@ -28,7 +28,7 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lz-string": "^1.5.0",
|
"lz-string": "^1.5.0",
|
||||||
"mutate-animate": "^1.4.2",
|
"mutate-animate": "^1.4.2",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.4.38"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.24.8",
|
"@babel/cli": "^7.24.8",
|
||||||
|
685
pnpm-lock.yaml
685
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,7 @@
|
|||||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||||
import {
|
import { defineComponent, onUpdated, shallowRef, watch } from 'vue';
|
||||||
defineComponent,
|
|
||||||
onMounted,
|
|
||||||
onUpdated,
|
|
||||||
ref,
|
|
||||||
shallowRef,
|
|
||||||
SlotsType,
|
|
||||||
VNode,
|
|
||||||
watch
|
|
||||||
} from 'vue';
|
|
||||||
import { Transform } from '../transform';
|
import { Transform } from '../transform';
|
||||||
import { isSetEqual } from '../utils';
|
import { isSetEqual } from '../utils';
|
||||||
import { logger } from '@/core/common/logger';
|
|
||||||
import { Sprite } from '../sprite';
|
|
||||||
import { onTick } from '../renderer';
|
|
||||||
import { isNil } from 'lodash-es';
|
|
||||||
import { SetupComponentOptions } from './types';
|
|
||||||
|
|
||||||
export const enum WordBreak {
|
export const enum WordBreak {
|
||||||
/** 不换行 */
|
/** 不换行 */
|
||||||
@ -32,22 +18,12 @@ export const enum TextAlign {
|
|||||||
End
|
End
|
||||||
}
|
}
|
||||||
|
|
||||||
let testCanvas: MotaOffscreenCanvas2D;
|
|
||||||
Mota.require('var', 'loading').once('coreInit', () => {
|
|
||||||
testCanvas = new MotaOffscreenCanvas2D(false);
|
|
||||||
testCanvas.withGameScale(false);
|
|
||||||
testCanvas.setHD(false);
|
|
||||||
testCanvas.size(32, 32);
|
|
||||||
testCanvas.freeze();
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface TextContentProps {
|
export interface TextContentProps {
|
||||||
text: string;
|
text: string;
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
/** 字体 */
|
|
||||||
font?: string;
|
font?: string;
|
||||||
/** 是否持续上一次的文本,开启后,如果修改后的文本以修改前的文本为开头,那么会继续播放而不会从头播放 */
|
/** 是否持续上一次的文本,开启后,如果修改后的文本以修改前的文本为开头,那么会继续播放而不会从头播放 */
|
||||||
keepLast?: boolean;
|
keepLast?: boolean;
|
||||||
@ -64,24 +40,9 @@ export interface TextContentProps {
|
|||||||
/** 行尾忽略字符,即不会出现在行尾的字符 */
|
/** 行尾忽略字符,即不会出现在行尾的字符 */
|
||||||
ignoreLineEnd?: Iterable<string>;
|
ignoreLineEnd?: Iterable<string>;
|
||||||
/** 会被分词规则识别的分词字符 */
|
/** 会被分词规则识别的分词字符 */
|
||||||
breakChars?: Iterable<string>;
|
breakChars: Iterable<string>;
|
||||||
/** 填充样式 */
|
|
||||||
fillStyle?: CanvasStyle;
|
|
||||||
/** 描边样式 */
|
|
||||||
strokeStyle?: CanvasStyle;
|
|
||||||
/** 线宽 */
|
|
||||||
strokeWidth?: number;
|
|
||||||
/** 是否填充 */
|
|
||||||
fill?: boolean;
|
|
||||||
/** 是否描边 */
|
|
||||||
stroke?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextContentEmits = {
|
|
||||||
typeEnd: () => void;
|
|
||||||
typeStart: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TextContentData {
|
interface TextContentData {
|
||||||
text: string;
|
text: string;
|
||||||
width: number;
|
width: number;
|
||||||
@ -96,396 +57,132 @@ interface TextContentData {
|
|||||||
breakChars: Set<string>;
|
breakChars: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextContentRenderable {
|
class TextContentCachePool {
|
||||||
x: number;
|
private pool: MotaOffscreenCanvas2D[] = [];
|
||||||
y: number;
|
|
||||||
/** 行高,为0时表示两行间为默认行距 */
|
|
||||||
height: number;
|
|
||||||
/** 这一行文字的高度,即 measureText 算出的高度 */
|
|
||||||
textHeight: number;
|
|
||||||
/** 这一行的文字 */
|
|
||||||
text: string;
|
|
||||||
/** 当前渲染到了本行的哪个文字 */
|
|
||||||
pointer: number;
|
|
||||||
/** 本行文字在全部文字中的起点 */
|
|
||||||
from: number;
|
|
||||||
/** 本行文字在全部文字中的终点 */
|
|
||||||
to: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const textContentOptions = {
|
/**
|
||||||
props: [
|
* 申请画布
|
||||||
'breakChars',
|
* @param num 要申请多少画布
|
||||||
'font',
|
*/
|
||||||
'height',
|
requestCanvas(num: number): MotaOffscreenCanvas2D[] {
|
||||||
'ignoreLineEnd',
|
if (this.pool.length < num) {
|
||||||
'ignoreLineStart',
|
const diff = num - this.pool.length;
|
||||||
'interval',
|
for (let i = 0; i < diff; i++) {
|
||||||
'keepLast',
|
this.pool.push(new MotaOffscreenCanvas2D(false));
|
||||||
'lineHeight',
|
}
|
||||||
'text',
|
|
||||||
'textAlign',
|
|
||||||
'width',
|
|
||||||
'wordBreak',
|
|
||||||
'x',
|
|
||||||
'y'
|
|
||||||
],
|
|
||||||
emits: ['typeEnd', 'typeStart']
|
|
||||||
} satisfies SetupComponentOptions<TextContentProps, TextContentEmits>;
|
|
||||||
|
|
||||||
export const TextContent = defineComponent<TextContentProps, TextContentEmits>(
|
|
||||||
(props, { emit }) => {
|
|
||||||
if (props.width && props.width <= 0) {
|
|
||||||
logger.warn(41, String(props.width));
|
|
||||||
}
|
}
|
||||||
const renderData: Required<TextContentProps> = {
|
return this.pool.splice(0, num);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退回画布
|
||||||
|
* @param canvas 要退回多少画布
|
||||||
|
*/
|
||||||
|
returnCanvas(canvas: MotaOffscreenCanvas2D[]) {
|
||||||
|
this.pool.push(...canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = new TextContentCachePool();
|
||||||
|
|
||||||
|
export const TextContent = defineComponent<TextContentProps>((props, ctx) => {
|
||||||
|
const ensureProps = () => {
|
||||||
|
props.x ??= 0;
|
||||||
|
props.y ??= 0;
|
||||||
|
props.width ??= 200;
|
||||||
|
props.height ??= 200;
|
||||||
|
props.font ??= core.status.globalAttribute.font;
|
||||||
|
props.ignoreLineEnd ??= new Set();
|
||||||
|
props.ignoreLineStart ??= new Set();
|
||||||
|
props.keepLast ??= false;
|
||||||
|
props.interval ??= 0;
|
||||||
|
props.lineHeight ??= 0;
|
||||||
|
props.wordBreak ??= WordBreak.Space;
|
||||||
|
props.breakChars ??= new Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeSplitData = (): TextContentData => {
|
||||||
|
ensureProps();
|
||||||
|
return {
|
||||||
text: props.text,
|
text: props.text,
|
||||||
textAlign: props.textAlign ?? TextAlign.Left,
|
width: props.width!,
|
||||||
x: props.x ?? 0,
|
font: props.font!,
|
||||||
y: props.y ?? 0,
|
wordBreak: props.wordBreak!,
|
||||||
width: !props.width || props.width <= 0 ? 200 : props.width,
|
ignoreLineStart: new Set(props.ignoreLineStart),
|
||||||
height: props.height ?? 200,
|
ignoreLineEnd: new Set(props.ignoreLineEnd),
|
||||||
font:
|
breakChars: new Set(props.breakChars)
|
||||||
props.font ??
|
|
||||||
core.status.globalAttribute?.font ??
|
|
||||||
'16px Verdana',
|
|
||||||
ignoreLineEnd: props.ignoreLineEnd ?? new Set(),
|
|
||||||
ignoreLineStart: props.ignoreLineStart ?? new Set(),
|
|
||||||
keepLast: props.keepLast ?? false,
|
|
||||||
interval: props.interval ?? 0,
|
|
||||||
lineHeight: props.lineHeight ?? 0,
|
|
||||||
wordBreak: props.wordBreak ?? WordBreak.Space,
|
|
||||||
breakChars: props.breakChars ?? new Set(),
|
|
||||||
fillStyle: props.fillStyle ?? '#fff',
|
|
||||||
strokeStyle: props.strokeStyle ?? 'transparent',
|
|
||||||
fill: props.fill ?? true,
|
|
||||||
stroke: props.stroke ?? false,
|
|
||||||
strokeWidth: props.strokeWidth ?? 2
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const ensureProps = () => {
|
/**
|
||||||
for (const [key, value] of Object.entries(props)) {
|
* 判断是否需要重新分行
|
||||||
if (key in renderData && !isNil(value)) {
|
*/
|
||||||
if (key === 'width') {
|
const needResplit = (value: TextContentData, old: TextContentData) => {
|
||||||
if (value && value <= 0) {
|
return (
|
||||||
logger.warn(41, String(props.width));
|
value.text !== old.text ||
|
||||||
renderData.width = 200;
|
value.font !== old.font ||
|
||||||
} else {
|
value.width !== old.width ||
|
||||||
renderData.width = value;
|
value.wordBreak !== old.wordBreak ||
|
||||||
}
|
!isSetEqual(value.breakChars, old.breakChars) ||
|
||||||
} else {
|
!isSetEqual(value.ignoreLineEnd, old.ignoreLineEnd) ||
|
||||||
// @ts-ignore
|
!isSetEqual(value.ignoreLineStart, old.ignoreLineStart)
|
||||||
renderData[key] = value;
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
const render = (canvas: MotaOffscreenCanvas2D, transform: Transform) => {};
|
||||||
|
|
||||||
|
const data = shallowRef<TextContentData>(makeSplitData());
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
data.value = makeSplitData();
|
||||||
|
});
|
||||||
|
|
||||||
|
let shouldKeep = false;
|
||||||
|
const lineData = shallowRef([0]);
|
||||||
|
watch(data, (value, old) => {
|
||||||
|
if (needResplit(value, old)) {
|
||||||
|
lineData.value = splitLines(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.keepLast && value.text.startsWith(old.text)) {
|
||||||
|
shouldKeep = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(lineData, (value, old) => {
|
||||||
|
if (shouldKeep) {
|
||||||
|
shouldKeep = false;
|
||||||
|
const isSub = value.every((v, i) => v === old[i]);
|
||||||
|
if (isSub) {
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const makeSplitData = (): TextContentData => {
|
return () => {
|
||||||
ensureProps();
|
return (
|
||||||
return {
|
<sprite
|
||||||
text: renderData.text,
|
x={props.x}
|
||||||
width: renderData.width!,
|
y={props.y}
|
||||||
font: renderData.font!,
|
width={props.width}
|
||||||
wordBreak: renderData.wordBreak!,
|
height={props.height}
|
||||||
ignoreLineStart: new Set(renderData.ignoreLineStart),
|
render={render}
|
||||||
ignoreLineEnd: new Set(renderData.ignoreLineEnd),
|
></sprite>
|
||||||
breakChars: new Set(renderData.breakChars)
|
);
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
export const Textbox = defineComponent((props, ctx) => {
|
||||||
* 判断是否需要重新分行
|
return () => {};
|
||||||
*/
|
});
|
||||||
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)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 每行的渲染信息 */
|
let testCanvas: MotaOffscreenCanvas2D;
|
||||||
const renderable: TextContentRenderable[] = [];
|
Mota.require('var', 'loading').once('coreInit', () => {
|
||||||
/** 需要更新的行数 */
|
testCanvas = new MotaOffscreenCanvas2D(false);
|
||||||
const dirtyIndex: number[] = [];
|
testCanvas.withGameScale(false);
|
||||||
const spriteElement = ref<Sprite>();
|
testCanvas.setHD(false);
|
||||||
|
testCanvas.size(32, 32);
|
||||||
/** dirtyIndex 更新指针 */
|
testCanvas.freeze();
|
||||||
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 {
|
|
||||||
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.stroke) ctx.strokeText(text, v.x, v.y);
|
|
||||||
if (renderData.fill) ctx.fillText(text, v.x, v.y);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成 renderable 对象
|
|
||||||
* @param text 全部文本
|
|
||||||
* @param lines 分行信息
|
|
||||||
* @param index 从第几行开始生成
|
|
||||||
* @param from 从第几个字符开始生成
|
|
||||||
*/
|
|
||||||
const makeRenderable = (
|
|
||||||
text: string,
|
|
||||||
lines: number[],
|
|
||||||
index: number,
|
|
||||||
from: number
|
|
||||||
) => {
|
|
||||||
renderable.splice(index);
|
|
||||||
dirtyIndex.splice(0);
|
|
||||||
dirtyIndex.push(index);
|
|
||||||
// 初始化渲染
|
|
||||||
linePointer = 0;
|
|
||||||
startTime = Date.now();
|
|
||||||
fromChar = from;
|
|
||||||
needUpdate = true;
|
|
||||||
|
|
||||||
let startY = renderable.reduce(
|
|
||||||
(prev, curr) => prev + curr.textHeight + curr.height,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
// 第一个比较特殊,需要特判
|
|
||||||
const start = lines[index - 1] ?? 0;
|
|
||||||
const end = lines[index];
|
|
||||||
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!);
|
|
||||||
startY += height;
|
|
||||||
|
|
||||||
renderable.push({
|
|
||||||
text: text.slice(start, end),
|
|
||||||
x: 0,
|
|
||||||
y: startY,
|
|
||||||
height: renderData.lineHeight!,
|
|
||||||
textHeight: height,
|
|
||||||
pointer: 0,
|
|
||||||
from: start,
|
|
||||||
to: end
|
|
||||||
});
|
|
||||||
}
|
|
||||||
emit('typeStart');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从头开始渲染
|
|
||||||
*/
|
|
||||||
const rawRender = (text: string, lines: number[]) => {
|
|
||||||
makeRenderable(text, lines, 0, 0);
|
|
||||||
spriteElement.value?.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 接续上一个继续渲染
|
|
||||||
* @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());
|
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
data.value = makeSplitData();
|
|
||||||
});
|
|
||||||
|
|
||||||
let shouldKeep = false;
|
|
||||||
const lineData = shallowRef([0]);
|
|
||||||
watch(data, (value, old) => {
|
|
||||||
if (needResplit(value, old)) {
|
|
||||||
lineData.value = splitLines(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderData.keepLast && value.text.startsWith(old.text)) {
|
|
||||||
shouldKeep = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 判断是否需要接续渲染
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rawRender(data.value.text, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<sprite
|
|
||||||
ref={spriteElement}
|
|
||||||
hd
|
|
||||||
antiAliasing={false}
|
|
||||||
x={renderData.x}
|
|
||||||
y={renderData.y}
|
|
||||||
width={renderData.width}
|
|
||||||
height={renderData.height}
|
|
||||||
render={renderContent}
|
|
||||||
></sprite>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
textContentOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface TextboxProps extends TextContentProps {
|
|
||||||
/** 背景颜色 */
|
|
||||||
backColor?: CanvasStyle;
|
|
||||||
/** 背景 winskin */
|
|
||||||
winskin?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextboxSlots extends SlotsType {
|
|
||||||
default: () => VNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const textboxOptions = {
|
|
||||||
props: (textContentOptions.props as (keyof TextboxProps)[]).concat([
|
|
||||||
'backColor',
|
|
||||||
'winskin'
|
|
||||||
])
|
|
||||||
} satisfies SetupComponentOptions<TextboxProps, {}, string, TextboxSlots>;
|
|
||||||
|
|
||||||
export const Textbox = defineComponent<TextboxProps, {}, string, TextboxSlots>(
|
|
||||||
(props, { slots }) => {
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<container>
|
|
||||||
{slots.default ? (
|
|
||||||
slots.default()
|
|
||||||
) : props.winskin ? (
|
|
||||||
// todo
|
|
||||||
<winskin></winskin>
|
|
||||||
) : (
|
|
||||||
// todo
|
|
||||||
<g-rect
|
|
||||||
x={0}
|
|
||||||
y={0}
|
|
||||||
width={props.width ?? 200}
|
|
||||||
height={props.height ?? 200}
|
|
||||||
fill
|
|
||||||
fillStyle={props.backColor}
|
|
||||||
></g-rect>
|
|
||||||
)}
|
|
||||||
<TextContent {...props}></TextContent>
|
|
||||||
</container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
textboxOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const fontSizeGuessScale = new Map<string, number>([
|
const fontSizeGuessScale = new Map<string, number>([
|
||||||
['px', 1],
|
['px', 1],
|
||||||
@ -510,8 +207,7 @@ const fontSizeGuessScale = new Map<string, number>([
|
|||||||
*/
|
*/
|
||||||
function splitLines(data: TextContentData) {
|
function splitLines(data: TextContentData) {
|
||||||
const words = breakWords(data);
|
const words = breakWords(data);
|
||||||
if (words.length === 0) return [];
|
if (words.length === 1) return [words[0]];
|
||||||
else if (words.length === 1) return [words[0]];
|
|
||||||
|
|
||||||
// 对文字二分,然后计算长度
|
// 对文字二分,然后计算长度
|
||||||
const text = data.text;
|
const text = data.text;
|
||||||
@ -519,9 +215,9 @@ function splitLines(data: TextContentData) {
|
|||||||
const fontSize = data.font.match(/\s*[\d\.-]+[a-zA-Z%]*\s*/)?.[0].trim();
|
const fontSize = data.font.match(/\s*[\d\.-]+[a-zA-Z%]*\s*/)?.[0].trim();
|
||||||
const unit = fontSize?.match(/[a-zA-Z%]+/)?.[0];
|
const unit = fontSize?.match(/[a-zA-Z%]+/)?.[0];
|
||||||
const guessScale = fontSizeGuessScale.get(unit ?? '') ?? 0.2;
|
const guessScale = fontSizeGuessScale.get(unit ?? '') ?? 0.2;
|
||||||
const guessSize = parseInt(fontSize ?? '1') * guessScale;
|
const guessSize = parseInt(fontSize ?? '0') * guessScale;
|
||||||
const averageLength = text.length / words.length;
|
const averageLength = text.length / words.length;
|
||||||
const guess = Math.max(data.width / guessSize / averageLength, 1);
|
const guess = data.width / guessSize / averageLength;
|
||||||
const ctx = testCanvas.ctx;
|
const ctx = testCanvas.ctx;
|
||||||
ctx.font = data.font;
|
ctx.font = data.font;
|
||||||
|
|
||||||
@ -532,6 +228,7 @@ function splitLines(data: TextContentData) {
|
|||||||
let guessCount = 1;
|
let guessCount = 1;
|
||||||
let splitProgress = false;
|
let splitProgress = false;
|
||||||
|
|
||||||
|
console.time();
|
||||||
while (1) {
|
while (1) {
|
||||||
if (!splitProgress) {
|
if (!splitProgress) {
|
||||||
const chars = text.slice(words[start], words[end]);
|
const chars = text.slice(words[start], words[end]);
|
||||||
@ -549,10 +246,9 @@ function splitLines(data: TextContentData) {
|
|||||||
|
|
||||||
if (diff === 1) {
|
if (diff === 1) {
|
||||||
res.push(words[start]);
|
res.push(words[start]);
|
||||||
if (end >= words.length) break;
|
if (end === words.length) break;
|
||||||
resolved = start;
|
resolved = start;
|
||||||
end = Math.ceil(start + guess);
|
end = Math.ceil(start + guess);
|
||||||
if (end > words.length) end = words.length;
|
|
||||||
guessCount = 1;
|
guessCount = 1;
|
||||||
splitProgress = false;
|
splitProgress = false;
|
||||||
} else {
|
} else {
|
||||||
@ -568,6 +264,7 @@ function splitLines(data: TextContentData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.timeEnd();
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -605,7 +302,6 @@ function isCJK(char: number) {
|
|||||||
* @returns 一个数字数组,每一项应当在这一项索引之后分词
|
* @returns 一个数字数组,每一项应当在这一项索引之后分词
|
||||||
*/
|
*/
|
||||||
function breakWords(data: TextContentData) {
|
function breakWords(data: TextContentData) {
|
||||||
if (data.text.length <= 1) return [data.text.length];
|
|
||||||
let allBreak = false;
|
let allBreak = false;
|
||||||
const breakChars = breakSet.union(data.breakChars);
|
const breakChars = breakSet.union(data.breakChars);
|
||||||
switch (data.wordBreak) {
|
switch (data.wordBreak) {
|
||||||
@ -636,7 +332,7 @@ function breakWords(data: TextContentData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ignoreLineStart.has(char) && !ignoreLineStart.has(next)) {
|
if (ignoreLineStart.has(char) && !ignoreLineStart.has(next)) {
|
||||||
res.push(pointer + 1);
|
res.push(pointer);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,16 +342,10 @@ function breakWords(data: TextContentData) {
|
|||||||
char === '\n' ||
|
char === '\n' ||
|
||||||
isCJK(char.charCodeAt(0))
|
isCJK(char.charCodeAt(0))
|
||||||
) {
|
) {
|
||||||
res.push(pointer + 1);
|
res.push(pointer);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.push(text.length);
|
res.push(text.length);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHeight(text: string, font: string) {
|
|
||||||
const ctx = testCanvas.ctx;
|
|
||||||
ctx.font = font;
|
|
||||||
return ctx.measureText(text).fontBoundingBoxAscent;
|
|
||||||
}
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { ComponentOptions, EmitsOptions, SlotsType } from 'vue';
|
|
||||||
|
|
||||||
export type SetupComponentOptions<
|
|
||||||
Props extends Record<string, any>,
|
|
||||||
E extends EmitsOptions = {},
|
|
||||||
EE extends string = string,
|
|
||||||
S extends SlotsType = {}
|
|
||||||
> = Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
|
|
||||||
props?: (keyof Props)[];
|
|
||||||
emits?: E | EE[];
|
|
||||||
slots?: S;
|
|
||||||
};
|
|
@ -11,8 +11,8 @@ import { LayerGroupHalo } from '@/plugin/fx/halo';
|
|||||||
import { FloorViewport } from './preset/viewport';
|
import { FloorViewport } from './preset/viewport';
|
||||||
import { PopText } from '@/plugin/fx/pop';
|
import { PopText } from '@/plugin/fx/pop';
|
||||||
import { FloorChange } from '@/plugin/fallback';
|
import { FloorChange } from '@/plugin/fallback';
|
||||||
import { createApp } from './renderer';
|
import { render } from './renderer';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
|
|
||||||
let main: MotaRenderer;
|
let main: MotaRenderer;
|
||||||
|
|
||||||
@ -61,8 +61,7 @@ Mota.require('var', 'loading').once('coreInit', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
main.hide();
|
main.hide();
|
||||||
createApp(Com).mount(main);
|
render(<Com></Com>, main);
|
||||||
// render(<Com></Com>, main);
|
|
||||||
|
|
||||||
console.log(main);
|
console.log(main);
|
||||||
});
|
});
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import { logger } from '../common/logger';
|
|
||||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
|
||||||
|
|
||||||
export class CanvasPool {
|
|
||||||
private pool: MotaOffscreenCanvas2D[] = [];
|
|
||||||
private requested: Set<MotaOffscreenCanvas2D> = new Set();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 申请画布
|
|
||||||
* @param num 要申请多少画布
|
|
||||||
*/
|
|
||||||
requestCanvas(num: number): MotaOffscreenCanvas2D[] {
|
|
||||||
if (this.pool.length < num) {
|
|
||||||
const diff = num - this.pool.length;
|
|
||||||
for (let i = 0; i < diff; i++) {
|
|
||||||
this.pool.push(new MotaOffscreenCanvas2D(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const toProvide = this.pool.splice(0, num);
|
|
||||||
toProvide.forEach(v => this.requested.add(v));
|
|
||||||
return toProvide;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退回画布
|
|
||||||
* @param canvas 要退回多少画布
|
|
||||||
*/
|
|
||||||
returnCanvas(canvas: MotaOffscreenCanvas2D[]) {
|
|
||||||
canvas.forEach(v => {
|
|
||||||
if (!this.requested.has(v)) {
|
|
||||||
logger.warn(40);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.requested.delete(v);
|
|
||||||
this.pool.push(v);
|
|
||||||
v.clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.pool.forEach(v => v.delete());
|
|
||||||
this.requested.forEach(v => v.delete());
|
|
||||||
}
|
|
||||||
}
|
|
@ -84,27 +84,11 @@ 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.update(v));
|
||||||
});
|
});
|
||||||
|
@ -92,7 +92,6 @@ const standardElementNoCache = (
|
|||||||
|
|
||||||
// Default elements
|
// Default elements
|
||||||
tagMap.register('container', standardElement(Container));
|
tagMap.register('container', standardElement(Container));
|
||||||
tagMap.register('template', standardElement(Container));
|
|
||||||
tagMap.register('mota-renderer', (_0, _1, props) => {
|
tagMap.register('mota-renderer', (_0, _1, props) => {
|
||||||
return new MotaRenderer(props?.id);
|
return new MotaRenderer(props?.id);
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
ILayerRenderExtends
|
ILayerRenderExtends
|
||||||
} from '../preset/layer';
|
} from '../preset/layer';
|
||||||
import type { EnemyCollection } from '@/game/enemy/damage';
|
import type { EnemyCollection } from '@/game/enemy/damage';
|
||||||
import { ILineProperty } from '../preset/graphics';
|
|
||||||
|
|
||||||
export interface CustomProps {
|
export interface CustomProps {
|
||||||
_item: (props: BaseProps) => RenderItem;
|
_item: (props: BaseProps) => RenderItem;
|
||||||
@ -91,34 +90,19 @@ export interface DamageProps extends BaseProps {
|
|||||||
strokeWidth?: number;
|
strokeWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphicPropsBase extends BaseProps, Partial<ILineProperty> {
|
export interface RectProps extends BaseProps {}
|
||||||
/** 是否填充,若填写 {@link stroke},那么表现为先填充后描边 */
|
|
||||||
fill?: boolean;
|
|
||||||
/** 是否描边,若填写 {@link fill},那么表现为先填充后描边 */
|
|
||||||
stroke?: boolean;
|
|
||||||
/** 是否先描边后填充,优先级最高,若设置,则 {@link fill} 与 {@link stroke} 无效。 */
|
|
||||||
strokeAndFill?: boolean;
|
|
||||||
/** 填充原则,比如 `nonzero` 表示非零环绕原则,默认为奇偶环绕原则 `evenodd` */
|
|
||||||
fillRule?: CanvasFillRule;
|
|
||||||
/** 填充样式 */
|
|
||||||
fillStyle?: CanvasStyle;
|
|
||||||
/** 描边样式 */
|
|
||||||
strokeStyle?: CanvasStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RectProps extends GraphicPropsBase {}
|
export interface CirclesProps extends BaseProps {}
|
||||||
|
|
||||||
export interface CirclesProps extends GraphicPropsBase {}
|
export interface EllipseProps extends BaseProps {}
|
||||||
|
|
||||||
export interface EllipseProps extends GraphicPropsBase {}
|
export interface LineProps extends BaseProps {}
|
||||||
|
|
||||||
export interface LineProps extends GraphicPropsBase {}
|
export interface BezierProps extends BaseProps {}
|
||||||
|
|
||||||
export interface BezierProps extends GraphicPropsBase {}
|
export interface QuadraticProps extends BaseProps {}
|
||||||
|
|
||||||
export interface QuadraticProps extends GraphicPropsBase {}
|
export interface PathProps extends BaseProps {}
|
||||||
|
|
||||||
export interface PathProps extends GraphicPropsBase {}
|
|
||||||
|
|
||||||
export interface IconProps extends BaseProps {}
|
export interface IconProps extends BaseProps {}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ export class Sprite<
|
|||||||
): void {
|
): void {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'render':
|
case 'render':
|
||||||
if (!this.assertType(nextValue, 'function', key)) return;
|
if (this.assertType(nextValue, 'function', key)) return;
|
||||||
this.setRenderFn(nextValue);
|
this.setRenderFn(nextValue);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,5 @@ export function multiplyTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
|
|||||||
* 判断两个集合是否相等
|
* 判断两个集合是否相等
|
||||||
*/
|
*/
|
||||||
export function isSetEqual<T>(set1: Set<T>, set2: Set<T>) {
|
export function isSetEqual<T>(set1: Set<T>, set2: Set<T>) {
|
||||||
if (set1 === set2) return true;
|
return set1.size === set2.size && set1.isSubsetOf(set2);
|
||||||
else return set1.size === set2.size && set1.isSubsetOf(set2);
|
|
||||||
}
|
}
|
||||||
|
@ -67,8 +67,6 @@
|
|||||||
"37": "Cannot execute 'requestSort' on plain render item, please ensure you have overrided 'requestSort' method in your own element.",
|
"37": "Cannot execute 'requestSort' on plain render item, please ensure you have overrided 'requestSort' method in your own element.",
|
||||||
"38": "Using plain text in jsx is strongly not recommended, since you can hardly modify its attributes. Consider using Text element instead.",
|
"38": "Using plain text in jsx is strongly not recommended, since you can hardly modify its attributes. Consider using Text element instead.",
|
||||||
"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.",
|
|
||||||
"41": "Width of text content components must be positive. receive: $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