mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-18 17:48:52 +08:00
feat: ConfirmBox 组件
This commit is contained in:
parent
e232a469c8
commit
0b2db0270c
@ -7,6 +7,9 @@ import { isNil } from 'lodash-es';
|
|||||||
import { logger } from '@/core/common/logger';
|
import { logger } from '@/core/common/logger';
|
||||||
import { IAnimateFrame, renderEmits } from '../frame';
|
import { IAnimateFrame, renderEmits } from '../frame';
|
||||||
|
|
||||||
|
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
||||||
|
const SAFE_PAD = 1;
|
||||||
|
|
||||||
type CanvasStyle = string | CanvasGradient | CanvasPattern;
|
type CanvasStyle = string | CanvasGradient | CanvasPattern;
|
||||||
|
|
||||||
export interface ETextEvent extends ERenderItemEvent {
|
export interface ETextEvent extends ERenderItemEvent {
|
||||||
@ -31,8 +34,10 @@ export class Text extends RenderItem<ETextEvent> {
|
|||||||
|
|
||||||
this.text = text;
|
this.text = text;
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
this.calBox();
|
this.requestBeforeFrame(() => {
|
||||||
this.emit('setText', text, this.width, this.height);
|
this.calBox();
|
||||||
|
this.emit('setText', text, this.width, this.height);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +46,7 @@ export class Text extends RenderItem<ETextEvent> {
|
|||||||
_transform: Transform
|
_transform: Transform
|
||||||
): void {
|
): void {
|
||||||
const ctx = canvas.ctx;
|
const ctx = canvas.ctx;
|
||||||
|
const stroke = this.strokeWidth;
|
||||||
ctx.textBaseline = 'bottom';
|
ctx.textBaseline = 'bottom';
|
||||||
ctx.fillStyle = this.fillStyle ?? 'transparent';
|
ctx.fillStyle = this.fillStyle ?? 'transparent';
|
||||||
ctx.strokeStyle = this.strokeStyle ?? 'transparent';
|
ctx.strokeStyle = this.strokeStyle ?? 'transparent';
|
||||||
@ -48,10 +54,10 @@ export class Text extends RenderItem<ETextEvent> {
|
|||||||
ctx.lineWidth = this.strokeWidth;
|
ctx.lineWidth = this.strokeWidth;
|
||||||
|
|
||||||
if (this.strokeStyle) {
|
if (this.strokeStyle) {
|
||||||
ctx.strokeText(this.text, 0, this.descent);
|
ctx.strokeText(this.text, stroke, this.descent + stroke + SAFE_PAD);
|
||||||
}
|
}
|
||||||
if (this.fillStyle) {
|
if (this.fillStyle) {
|
||||||
ctx.fillText(this.text, 0, this.descent);
|
ctx.fillText(this.text, stroke, this.descent + stroke + SAFE_PAD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +101,7 @@ export class Text extends RenderItem<ETextEvent> {
|
|||||||
setStyle(fill?: CanvasStyle, stroke?: CanvasStyle) {
|
setStyle(fill?: CanvasStyle, stroke?: CanvasStyle) {
|
||||||
this.fillStyle = fill;
|
this.fillStyle = fill;
|
||||||
this.strokeStyle = stroke;
|
this.strokeStyle = stroke;
|
||||||
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,7 +109,11 @@ export class Text extends RenderItem<ETextEvent> {
|
|||||||
* @param width 宽度
|
* @param width 宽度
|
||||||
*/
|
*/
|
||||||
setStrokeWidth(width: number) {
|
setStrokeWidth(width: number) {
|
||||||
|
const before = this.strokeWidth;
|
||||||
this.strokeWidth = width;
|
this.strokeWidth = width;
|
||||||
|
const dw = width - before;
|
||||||
|
this.size(this.width + dw * 2, this.height + dw * 2);
|
||||||
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,7 +124,8 @@ 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);
|
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
|
||||||
|
this.size(width, height + this.strokeWidth * 2 + SAFE_PAD * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleProps(
|
protected handleProps(
|
||||||
|
@ -564,10 +564,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
|
|||||||
private toTagString(item: RenderItem, space: number, deep: number): string {
|
private toTagString(item: RenderItem, space: number, deep: number): string {
|
||||||
const name = item.constructor.name;
|
const name = item.constructor.name;
|
||||||
if (item.children.size === 0) {
|
if (item.children.size === 0) {
|
||||||
return `${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}"${item.hidden ? ' hidden' : ''}></${name}>\n`;
|
return `${' '.repeat(deep * space)}<${name} id="${item.id}" uid="${item.uid}" type="${item.type}"${item.hidden ? ' hidden' : ''}></${name}>\n`;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
`${' '.repeat(deep * space)}<${name} id="${item.id}" type="${item.type}" ${item.hidden ? 'hidden' : ''}>\n` +
|
`${' '.repeat(deep * space)}<${name} id="${item.id}" uid="${item.uid}" type="${item.type}" ${item.hidden ? 'hidden' : ''}>\n` +
|
||||||
`${[...item.children].map(v => this.toTagString(v, space, deep + 1)).join('')}` +
|
`${[...item.children].map(v => this.toTagString(v, space, deep + 1)).join('')}` +
|
||||||
`${' '.repeat(deep * space)}</${name}>\n`
|
`${' '.repeat(deep * space)}</${name}>\n`
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,164 @@
|
|||||||
import { DefaultProps } from '@/core/render';
|
import { DefaultProps, ElementLocator, useKey } from '@/core/render';
|
||||||
import { defineComponent } from 'vue';
|
import { computed, defineComponent, ref } from 'vue';
|
||||||
|
import { Background, Selection } from './misc';
|
||||||
|
import { TextContent, TextContentExpose, TextContentProps } from './textbox';
|
||||||
|
import { SetupComponentOptions } from './types';
|
||||||
|
import { TextAlign } from './textboxTyper';
|
||||||
|
|
||||||
export interface ConfirmBoxProps extends DefaultProps {
|
export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
|
||||||
text: string;
|
text: string;
|
||||||
|
width: number;
|
||||||
|
loc: ElementLocator;
|
||||||
|
selFont?: string;
|
||||||
|
selFill?: CanvasStyle;
|
||||||
|
pad?: number;
|
||||||
yesText?: string;
|
yesText?: string;
|
||||||
noText?: string;
|
noText?: string;
|
||||||
winskin?: string;
|
winskin?: ImageIds;
|
||||||
|
defaultYes?: boolean;
|
||||||
|
color?: CanvasStyle;
|
||||||
|
border?: CanvasStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfirmBoxEmits {
|
export type ConfirmBoxEmits = {
|
||||||
onYes: () => void;
|
yes: () => void;
|
||||||
onNo: () => void;
|
no: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ConfirmBox = defineComponent(() => {
|
const confirmBoxProps = {
|
||||||
return () => <container></container>;
|
props: [
|
||||||
});
|
'text',
|
||||||
|
'width',
|
||||||
|
'loc',
|
||||||
|
'selFont',
|
||||||
|
'selFill',
|
||||||
|
'pad',
|
||||||
|
'yesText',
|
||||||
|
'noText',
|
||||||
|
'winskin',
|
||||||
|
'defaultYes',
|
||||||
|
'color',
|
||||||
|
'border'
|
||||||
|
],
|
||||||
|
emits: ['no', 'yes']
|
||||||
|
} satisfies SetupComponentOptions<
|
||||||
|
ConfirmBoxProps,
|
||||||
|
ConfirmBoxEmits,
|
||||||
|
keyof ConfirmBoxEmits
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ConfirmBox = defineComponent<
|
||||||
|
ConfirmBoxProps,
|
||||||
|
ConfirmBoxEmits,
|
||||||
|
keyof ConfirmBoxEmits
|
||||||
|
>((props, { emit, attrs }) => {
|
||||||
|
const content = ref<TextContentExpose>();
|
||||||
|
const height = ref(200);
|
||||||
|
const selected = ref(props.defaultYes ? true : false);
|
||||||
|
const yesSize = ref<[number, number]>([0, 0]);
|
||||||
|
const noSize = ref<[number, number]>([0, 0]);
|
||||||
|
|
||||||
|
const loc = computed<ElementLocator>(() => {
|
||||||
|
const [x = 0, y = 0, , , ax = 0, ay = 0] = props.loc;
|
||||||
|
return [x, y, props.width, height.value, ax, ay];
|
||||||
|
});
|
||||||
|
const yesText = computed(() => props.yesText ?? '确认');
|
||||||
|
const noText = computed(() => props.noText ?? '取消');
|
||||||
|
const pad = computed(() => props.pad ?? 32);
|
||||||
|
const yesLoc = computed<ElementLocator>(() => {
|
||||||
|
const y = height.value - pad.value;
|
||||||
|
return [props.width / 3, y, void 0, void 0, 0.5, 1];
|
||||||
|
});
|
||||||
|
const noLoc = computed<ElementLocator>(() => {
|
||||||
|
const y = height.value - pad.value;
|
||||||
|
return [(props.width / 3) * 2, y, void 0, void 0, 0.5, 1];
|
||||||
|
});
|
||||||
|
const contentLoc = computed<ElementLocator>(() => {
|
||||||
|
const width = props.width - pad.value * 2;
|
||||||
|
return [props.width / 2, pad.value, width, 0, 0.5, 0];
|
||||||
|
});
|
||||||
|
const selectLoc = computed<ElementLocator>(() => {
|
||||||
|
if (selected.value) {
|
||||||
|
const [x = 0, y = 0] = yesLoc.value;
|
||||||
|
const [width, height] = yesSize.value;
|
||||||
|
return [x, y + 4, width + 8, height + 8, 0.5, 1];
|
||||||
|
} else {
|
||||||
|
const [x = 0, y = 0] = noLoc.value;
|
||||||
|
const [width, height] = noSize.value;
|
||||||
|
return [x, y + 4, width + 8, height + 8, 0.5, 1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUpdateHeight = (textHeight: number) => {
|
||||||
|
height.value = textHeight + pad.value * 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setYes = (_: string, width: number, height: number) => {
|
||||||
|
yesSize.value = [width, height];
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNo = (_: string, width: number, height: number) => {
|
||||||
|
noSize.value = [width, height];
|
||||||
|
};
|
||||||
|
|
||||||
|
const [key] = useKey();
|
||||||
|
key.realize('confirm', () => {
|
||||||
|
if (selected.value) emit('yes');
|
||||||
|
else emit('no');
|
||||||
|
});
|
||||||
|
key.realize('moveLeft', () => void (selected.value = true));
|
||||||
|
key.realize('moveRight', () => void (selected.value = false));
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<container loc={loc.value}>
|
||||||
|
<Background
|
||||||
|
loc={[0, 0, props.width, height.value]}
|
||||||
|
winskin={props.winskin}
|
||||||
|
color={props.color}
|
||||||
|
border={props.border}
|
||||||
|
zIndex={0}
|
||||||
|
/>
|
||||||
|
<TextContent
|
||||||
|
{...attrs}
|
||||||
|
ref={content}
|
||||||
|
loc={contentLoc.value}
|
||||||
|
text={props.text}
|
||||||
|
width={props.width - pad.value * 2}
|
||||||
|
zIndex={5}
|
||||||
|
textAlign={TextAlign.Center}
|
||||||
|
autoHeight
|
||||||
|
onUpdateHeight={onUpdateHeight}
|
||||||
|
/>
|
||||||
|
<Selection
|
||||||
|
loc={selectLoc.value}
|
||||||
|
winskin={props.winskin}
|
||||||
|
color={props.color}
|
||||||
|
border={props.border}
|
||||||
|
noevent
|
||||||
|
zIndex={10}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
loc={yesLoc.value}
|
||||||
|
text={yesText.value}
|
||||||
|
fillStyle={props.selFill}
|
||||||
|
font={props.selFont}
|
||||||
|
cursor="pointer"
|
||||||
|
zIndex={15}
|
||||||
|
onClick={() => emit('yes')}
|
||||||
|
onEnter={() => (selected.value = true)}
|
||||||
|
onSetText={setYes}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
loc={noLoc.value}
|
||||||
|
text={noText.value}
|
||||||
|
fillStyle={props.selFill}
|
||||||
|
font={props.selFont}
|
||||||
|
cursor="pointer"
|
||||||
|
zIndex={15}
|
||||||
|
onClick={() => emit('no')}
|
||||||
|
onEnter={() => (selected.value = false)}
|
||||||
|
onSetText={setNo}
|
||||||
|
/>
|
||||||
|
</container>
|
||||||
|
);
|
||||||
|
}, confirmBoxProps);
|
||||||
|
@ -212,7 +212,7 @@ export const ScrollText = defineComponent<
|
|||||||
const scroll = ref<ScrollExpose>();
|
const scroll = ref<ScrollExpose>();
|
||||||
const speed = ref(props.speed);
|
const speed = ref(props.speed);
|
||||||
|
|
||||||
const eleHeight = computed(() => props.loc[3] ?? props.height ?? 200);
|
const eleHeight = computed(() => props.loc[3] ?? props.width);
|
||||||
const pad = computed(() => props.pad ?? 16);
|
const pad = computed(() => props.pad ?? 16);
|
||||||
|
|
||||||
let lastFixedTime = Date.now();
|
let lastFixedTime = Date.now();
|
||||||
@ -370,6 +370,15 @@ const backgroundProps = {
|
|||||||
props: ['loc', 'winskin', 'color', 'border']
|
props: ['loc', 'winskin', 'color', 'border']
|
||||||
} satisfies SetupComponentOptions<BackgroundProps>;
|
} satisfies SetupComponentOptions<BackgroundProps>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景组件,与 Selection 类似,不过绘制的是背景,而不是选择光标,参数参考 {@link BackgroundProps},用例如下:
|
||||||
|
* ```tsx
|
||||||
|
* // 使用 winskin2.png 作为背景
|
||||||
|
* <Background loc={[8, 8, 160, 160]} winskin="winskin2.png" />
|
||||||
|
* // 使用指定填充和边框颜色作为背景
|
||||||
|
* <Background loc={[8, 8, 160, 160]} color="#333" border="gold" />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export const Background = defineComponent<BackgroundProps>(props => {
|
export const Background = defineComponent<BackgroundProps>(props => {
|
||||||
const isWinskin = computed(() => !!props.winskin);
|
const isWinskin = computed(() => !!props.winskin);
|
||||||
const fixedLoc = computed<ElementLocator>(() => {
|
const fixedLoc = computed<ElementLocator>(() => {
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
ref,
|
ref,
|
||||||
shallowReactive,
|
shallowReactive,
|
||||||
@ -46,6 +47,7 @@ export interface TextContentProps
|
|||||||
export type TextContentEmits = {
|
export type TextContentEmits = {
|
||||||
typeEnd: () => void;
|
typeEnd: () => void;
|
||||||
typeStart: () => void;
|
typeStart: () => void;
|
||||||
|
updateHeight: (height: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TextContentExpose {
|
export interface TextContentExpose {
|
||||||
@ -89,7 +91,7 @@ const textContentOptions = {
|
|||||||
'width',
|
'width',
|
||||||
'autoHeight'
|
'autoHeight'
|
||||||
],
|
],
|
||||||
emits: ['typeEnd', 'typeStart']
|
emits: ['typeEnd', 'typeStart', 'updateHeight']
|
||||||
} satisfies SetupComponentOptions<
|
} satisfies SetupComponentOptions<
|
||||||
TextContentProps,
|
TextContentProps,
|
||||||
TextContentEmits,
|
TextContentEmits,
|
||||||
@ -144,10 +146,12 @@ export const TextContent = defineComponent<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateLoc = () => {
|
const updateLoc = () => {
|
||||||
|
const height = getHeight();
|
||||||
if (props.autoHeight) {
|
if (props.autoHeight) {
|
||||||
const [x = 0, y = 0, width = 200, , ax = 0, ay = 0] = loc.value;
|
const [x = 0, y = 0, width = 200, , ax = 0, ay = 0] = loc.value;
|
||||||
loc.value = [x, y, width, getHeight(), ax, ay];
|
loc.value = [x, y, width, height, ax, ay];
|
||||||
}
|
}
|
||||||
|
emit('updateHeight', height);
|
||||||
};
|
};
|
||||||
|
|
||||||
expose<TextContentExpose>({ retype, showAll, getHeight });
|
expose<TextContentExpose>({ retype, showAll, getHeight });
|
||||||
@ -198,6 +202,8 @@ export const TextContent = defineComponent<
|
|||||||
emit('typeEnd');
|
emit('typeEnd');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(retype);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return (
|
return (
|
||||||
<sprite
|
<sprite
|
||||||
|
@ -9,6 +9,9 @@ import {
|
|||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
|
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
||||||
|
const SAFE_PAD = 1;
|
||||||
|
|
||||||
export const enum WordBreak {
|
export const enum WordBreak {
|
||||||
/** 不换行 */
|
/** 不换行 */
|
||||||
None,
|
None,
|
||||||
@ -110,6 +113,8 @@ export interface ITextContentRenderable {
|
|||||||
export interface ITextContentRenderObject {
|
export interface ITextContentRenderObject {
|
||||||
/** 每一行的高度 */
|
/** 每一行的高度 */
|
||||||
lineHeights: number[];
|
lineHeights: number[];
|
||||||
|
/** 每一行的宽度 */
|
||||||
|
lineWidths: number[];
|
||||||
/** 渲染数据 */
|
/** 渲染数据 */
|
||||||
data: ITextContentRenderable[];
|
data: ITextContentRenderable[];
|
||||||
}
|
}
|
||||||
@ -173,7 +178,8 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
|||||||
/** 渲染信息 */
|
/** 渲染信息 */
|
||||||
private renderObject: ITextContentRenderObject = {
|
private renderObject: ITextContentRenderObject = {
|
||||||
lineHeights: [],
|
lineHeights: [],
|
||||||
data: []
|
data: [],
|
||||||
|
lineWidths: []
|
||||||
};
|
};
|
||||||
/** 渲染信息 */
|
/** 渲染信息 */
|
||||||
private renderData: TyperRenderable[] = [];
|
private renderData: TyperRenderable[] = [];
|
||||||
@ -239,7 +245,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
|||||||
getHeight() {
|
getHeight() {
|
||||||
const heights = this.renderObject.lineHeights;
|
const heights = this.renderObject.lineHeights;
|
||||||
const lines = heights.reduce((prev, curr) => prev + curr, 0);
|
const lines = heights.reduce((prev, curr) => prev + curr, 0);
|
||||||
return lines + this.config.lineHeight * heights.length;
|
return lines + this.config.lineHeight * heights.length + SAFE_PAD * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -275,7 +281,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
|||||||
this.typing = false;
|
this.typing = false;
|
||||||
this.dataLine = 0;
|
this.dataLine = 0;
|
||||||
this.x = 0;
|
this.x = 0;
|
||||||
this.y = 0;
|
this.y = SAFE_PAD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -288,6 +294,19 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
|||||||
this.renderObject = this.parser.parse(text, this.config.width);
|
this.renderObject = this.parser.parse(text, this.config.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDataX(line: number) {
|
||||||
|
const width = this.renderObject.lineWidths[line];
|
||||||
|
if (isNil(width)) return this.x;
|
||||||
|
switch (this.config.textAlign) {
|
||||||
|
case TextAlign.Left:
|
||||||
|
return this.x;
|
||||||
|
case TextAlign.Center:
|
||||||
|
return this.x + (this.config.width - width) / 2;
|
||||||
|
case TextAlign.End:
|
||||||
|
return this.x + this.config.width - width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@ -303,7 +322,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
|||||||
|
|
||||||
const data: TyperTextRenderable = {
|
const data: TyperTextRenderable = {
|
||||||
type: TextContentType.Text,
|
type: TextContentType.Text,
|
||||||
x: this.x,
|
x: this.getDataX(line),
|
||||||
y: this.y,
|
y: this.y,
|
||||||
text: renderable.text.slice(start, end),
|
text: renderable.text.slice(start, end),
|
||||||
font: renderable.font,
|
font: renderable.font,
|
||||||
@ -331,7 +350,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
|||||||
}
|
}
|
||||||
const data: TyperIconRenderable = {
|
const data: TyperIconRenderable = {
|
||||||
type: TextContentType.Icon,
|
type: TextContentType.Icon,
|
||||||
x: this.x,
|
x: this.getDataX(line),
|
||||||
y: this.y,
|
y: this.y,
|
||||||
width: iconWidth,
|
width: iconWidth,
|
||||||
height: iconWidth / aspect,
|
height: iconWidth / aspect,
|
||||||
@ -537,6 +556,8 @@ export class TextContentParser {
|
|||||||
private lineHeight: number = 0;
|
private lineHeight: number = 0;
|
||||||
/** 每一行的行高 */
|
/** 每一行的行高 */
|
||||||
private lineHeights: number[] = [];
|
private lineHeights: number[] = [];
|
||||||
|
/** 每一行的宽度 */
|
||||||
|
private lineWidths: number[] = [];
|
||||||
/** 当前这一行已经有多长 */
|
/** 当前这一行已经有多长 */
|
||||||
private lineWidth: number = 0;
|
private lineWidth: number = 0;
|
||||||
/** 这一行未计算部分的起始位置索引 */
|
/** 这一行未计算部分的起始位置索引 */
|
||||||
@ -809,6 +830,7 @@ export class TextContentParser {
|
|||||||
this.nowRenderable = -1;
|
this.nowRenderable = -1;
|
||||||
this.lineHeight = 0;
|
this.lineHeight = 0;
|
||||||
this.lineHeights = [];
|
this.lineHeights = [];
|
||||||
|
this.lineWidths = [];
|
||||||
this.lineWidth = 0;
|
this.lineWidth = 0;
|
||||||
this.lineStart = 0;
|
this.lineStart = 0;
|
||||||
this.guessGain = 1;
|
this.guessGain = 1;
|
||||||
@ -967,6 +989,7 @@ export class TextContentParser {
|
|||||||
const index = this.bsLineWidth(maxWidth, this.nowRenderable);
|
const index = this.bsLineWidth(maxWidth, this.nowRenderable);
|
||||||
data.splitLines.push(this.wordBreak[index]);
|
data.splitLines.push(this.wordBreak[index]);
|
||||||
this.lineHeights.push(this.lineHeight);
|
this.lineHeights.push(this.lineHeight);
|
||||||
|
this.lineWidths.push(this.lineWidth);
|
||||||
this.bsStart = index;
|
this.bsStart = index;
|
||||||
const text = data.text.slice(
|
const text = data.text.slice(
|
||||||
this.wordBreak[index] + 1,
|
this.wordBreak[index] + 1,
|
||||||
@ -990,10 +1013,11 @@ export class TextContentParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bsLineWidth(width: number, index: number) {
|
private bsLineWidth(maxWidth: number, index: number) {
|
||||||
let start = this.bsStart;
|
let start = this.bsStart;
|
||||||
let end = this.bsEnd;
|
let end = this.bsEnd;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
|
let width = 0;
|
||||||
|
|
||||||
const data = this.renderable[index];
|
const data = this.renderable[index];
|
||||||
const { wordBreak } = data;
|
const { wordBreak } = data;
|
||||||
@ -1005,6 +1029,7 @@ export class TextContentParser {
|
|||||||
if (height > this.lineHeight) {
|
if (height > this.lineHeight) {
|
||||||
this.lineHeight = height;
|
this.lineHeight = height;
|
||||||
}
|
}
|
||||||
|
this.lineWidth = width;
|
||||||
return start;
|
return start;
|
||||||
}
|
}
|
||||||
const text = data.text.slice(
|
const text = data.text.slice(
|
||||||
@ -1012,10 +1037,12 @@ export class TextContentParser {
|
|||||||
wordBreak[mid] + 1
|
wordBreak[mid] + 1
|
||||||
);
|
);
|
||||||
const metrics = ctx.measureText(text);
|
const metrics = ctx.measureText(text);
|
||||||
|
width = metrics.width;
|
||||||
height = this.getHeight(metrics);
|
height = this.getHeight(metrics);
|
||||||
if (metrics.width > width) {
|
if (width > maxWidth) {
|
||||||
end = mid;
|
end = mid;
|
||||||
} else if (metrics.width === width) {
|
} else if (width === maxWidth) {
|
||||||
|
this.lineWidth = width;
|
||||||
if (height > this.lineHeight) {
|
if (height > this.lineHeight) {
|
||||||
this.lineHeight = height;
|
this.lineHeight = height;
|
||||||
}
|
}
|
||||||
@ -1064,6 +1091,7 @@ export class TextContentParser {
|
|||||||
const index = this.bsLineWidth(maxWidth, pointer);
|
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.lineWidths.push(this.lineWidth);
|
||||||
this.bsStart = index;
|
this.bsStart = index;
|
||||||
const text = data.text.slice(this.wordBreak[index] + 1);
|
const text = data.text.slice(this.wordBreak[index] + 1);
|
||||||
if (!isLast && text.length < guess / 4) {
|
if (!isLast && text.length < guess / 4) {
|
||||||
@ -1089,9 +1117,9 @@ export class TextContentParser {
|
|||||||
let iconWidth = 0;
|
let iconWidth = 0;
|
||||||
if (aspect < 1) {
|
if (aspect < 1) {
|
||||||
// 这时候应该把高度限定在当前字体大小
|
// 这时候应该把高度限定在当前字体大小
|
||||||
iconWidth = width * (this.status.fontSize / height);
|
iconWidth = width * (data.fontSize / height);
|
||||||
} else {
|
} else {
|
||||||
iconWidth = this.status.fontSize;
|
iconWidth = data.fontSize;
|
||||||
}
|
}
|
||||||
this.lineWidth += iconWidth;
|
this.lineWidth += iconWidth;
|
||||||
const iconHeight = iconWidth / aspect;
|
const iconHeight = iconWidth / aspect;
|
||||||
@ -1109,13 +1137,69 @@ export class TextContentParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private checkLastSize() {
|
||||||
|
const last = this.renderable.at(-1);
|
||||||
|
if (!last) return;
|
||||||
|
const index = this.lastBreakIndex;
|
||||||
|
const text = last.text.slice(this.wordBreak[index] + 1);
|
||||||
|
const ctx = this.testCanvas.ctx;
|
||||||
|
ctx.font = last.font;
|
||||||
|
const metrics = ctx.measureText(text);
|
||||||
|
this.lineWidth = metrics.width;
|
||||||
|
const height = this.getHeight(metrics);
|
||||||
|
if (height > this.lineHeight) {
|
||||||
|
this.lineHeight = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkNoneBreakSize() {
|
||||||
|
const ctx = this.testCanvas.ctx;
|
||||||
|
this.renderable.forEach(data => {
|
||||||
|
switch (data.type) {
|
||||||
|
case TextContentType.Text: {
|
||||||
|
ctx.font = data.font;
|
||||||
|
const metrics = ctx.measureText(data.text);
|
||||||
|
this.lineWidth += metrics.width;
|
||||||
|
const height = this.getHeight(metrics);
|
||||||
|
if (height > this.lineHeight) this.lineHeight = height;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TextContentType.Icon: {
|
||||||
|
const renderable = texture.getRenderable(data.icon!);
|
||||||
|
if (!renderable) return false;
|
||||||
|
const [, , width, height] = renderable.render[0];
|
||||||
|
const aspect = width / height;
|
||||||
|
let iconWidth = 0;
|
||||||
|
if (aspect < 1) {
|
||||||
|
// 这时候应该把高度限定在当前字体大小
|
||||||
|
iconWidth = width * (data.fontSize / height);
|
||||||
|
} else {
|
||||||
|
iconWidth = data.fontSize;
|
||||||
|
}
|
||||||
|
this.lineWidth += iconWidth;
|
||||||
|
const iconHeight = iconWidth / aspect;
|
||||||
|
if (iconHeight > this.lineHeight) {
|
||||||
|
this.lineHeight = iconHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.lineHeights.push(this.lineHeight);
|
||||||
|
this.lineWidths.push(this.lineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对解析出的文字分词并分行
|
* 对解析出的文字分词并分行
|
||||||
* @param width 文字的宽度,到达这么宽之后换行
|
* @param width 文字的宽度,到达这么宽之后换行
|
||||||
*/
|
*/
|
||||||
private splitLines(width: number): ITextContentRenderObject {
|
private splitLines(width: number): ITextContentRenderObject {
|
||||||
if (this.wordBreakRule === WordBreak.None) {
|
if (this.wordBreakRule === WordBreak.None) {
|
||||||
return { lineHeights: [0], data: this.renderable };
|
this.checkNoneBreakSize();
|
||||||
|
return {
|
||||||
|
lineHeights: this.lineHeights,
|
||||||
|
data: this.renderable,
|
||||||
|
lineWidths: this.lineWidths
|
||||||
|
};
|
||||||
}
|
}
|
||||||
this.nowRenderable = -1;
|
this.nowRenderable = -1;
|
||||||
|
|
||||||
@ -1182,11 +1266,14 @@ export class TextContentParser {
|
|||||||
this.checkRestLine(width, guess, i);
|
this.checkRestLine(width, guess, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.checkLastSize();
|
||||||
this.lineHeights.push(this.lineHeight);
|
this.lineHeights.push(this.lineHeight);
|
||||||
|
this.lineWidths.push(this.lineWidth);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lineHeights: this.lineHeights,
|
lineHeights: this.lineHeights,
|
||||||
data: this.renderable
|
data: this.renderable,
|
||||||
|
lineWidths: this.lineWidths
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user