mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-11-04 15:12:58 +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 { IAnimateFrame, renderEmits } from '../frame';
 | 
			
		||||
 | 
			
		||||
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
 | 
			
		||||
const SAFE_PAD = 1;
 | 
			
		||||
 | 
			
		||||
type CanvasStyle = string | CanvasGradient | CanvasPattern;
 | 
			
		||||
 | 
			
		||||
export interface ETextEvent extends ERenderItemEvent {
 | 
			
		||||
@ -31,8 +34,10 @@ export class Text extends RenderItem<ETextEvent> {
 | 
			
		||||
 | 
			
		||||
        this.text = text;
 | 
			
		||||
        if (text.length > 0) {
 | 
			
		||||
            this.calBox();
 | 
			
		||||
            this.emit('setText', text, this.width, this.height);
 | 
			
		||||
            this.requestBeforeFrame(() => {
 | 
			
		||||
                this.calBox();
 | 
			
		||||
                this.emit('setText', text, this.width, this.height);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -41,6 +46,7 @@ export class Text extends RenderItem<ETextEvent> {
 | 
			
		||||
        _transform: Transform
 | 
			
		||||
    ): void {
 | 
			
		||||
        const ctx = canvas.ctx;
 | 
			
		||||
        const stroke = this.strokeWidth;
 | 
			
		||||
        ctx.textBaseline = 'bottom';
 | 
			
		||||
        ctx.fillStyle = this.fillStyle ?? 'transparent';
 | 
			
		||||
        ctx.strokeStyle = this.strokeStyle ?? 'transparent';
 | 
			
		||||
@ -48,10 +54,10 @@ export class Text extends RenderItem<ETextEvent> {
 | 
			
		||||
        ctx.lineWidth = this.strokeWidth;
 | 
			
		||||
 | 
			
		||||
        if (this.strokeStyle) {
 | 
			
		||||
            ctx.strokeText(this.text, 0, this.descent);
 | 
			
		||||
            ctx.strokeText(this.text, stroke, this.descent + stroke + SAFE_PAD);
 | 
			
		||||
        }
 | 
			
		||||
        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) {
 | 
			
		||||
        this.fillStyle = fill;
 | 
			
		||||
        this.strokeStyle = stroke;
 | 
			
		||||
        this.update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -102,7 +109,11 @@ export class Text extends RenderItem<ETextEvent> {
 | 
			
		||||
     * @param width 宽度
 | 
			
		||||
     */
 | 
			
		||||
    setStrokeWidth(width: number) {
 | 
			
		||||
        const before = this.strokeWidth;
 | 
			
		||||
        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.length = width;
 | 
			
		||||
        this.descent = actualBoundingBoxAscent;
 | 
			
		||||
        this.size(width, actualBoundingBoxAscent + actualBoundingBoxDescent);
 | 
			
		||||
        const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
 | 
			
		||||
        this.size(width, height + this.strokeWidth * 2 + SAFE_PAD * 2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected handleProps(
 | 
			
		||||
 | 
			
		||||
@ -564,10 +564,10 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
 | 
			
		||||
    private toTagString(item: RenderItem, space: number, deep: number): string {
 | 
			
		||||
        const name = item.constructor.name;
 | 
			
		||||
        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 {
 | 
			
		||||
            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('')}` +
 | 
			
		||||
                `${' '.repeat(deep * space)}</${name}>\n`
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,164 @@
 | 
			
		||||
import { DefaultProps } from '@/core/render';
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import { DefaultProps, ElementLocator, useKey } from '@/core/render';
 | 
			
		||||
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;
 | 
			
		||||
    width: number;
 | 
			
		||||
    loc: ElementLocator;
 | 
			
		||||
    selFont?: string;
 | 
			
		||||
    selFill?: CanvasStyle;
 | 
			
		||||
    pad?: number;
 | 
			
		||||
    yesText?: string;
 | 
			
		||||
    noText?: string;
 | 
			
		||||
    winskin?: string;
 | 
			
		||||
    winskin?: ImageIds;
 | 
			
		||||
    defaultYes?: boolean;
 | 
			
		||||
    color?: CanvasStyle;
 | 
			
		||||
    border?: CanvasStyle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ConfirmBoxEmits {
 | 
			
		||||
    onYes: () => void;
 | 
			
		||||
    onNo: () => void;
 | 
			
		||||
}
 | 
			
		||||
export type ConfirmBoxEmits = {
 | 
			
		||||
    yes: () => void;
 | 
			
		||||
    no: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ConfirmBox = defineComponent(() => {
 | 
			
		||||
    return () => <container></container>;
 | 
			
		||||
});
 | 
			
		||||
const confirmBoxProps = {
 | 
			
		||||
    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 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);
 | 
			
		||||
 | 
			
		||||
    let lastFixedTime = Date.now();
 | 
			
		||||
@ -370,6 +370,15 @@ const backgroundProps = {
 | 
			
		||||
    props: ['loc', 'winskin', 'color', 'border']
 | 
			
		||||
} 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 => {
 | 
			
		||||
    const isWinskin = computed(() => !!props.winskin);
 | 
			
		||||
    const fixedLoc = computed<ElementLocator>(() => {
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import {
 | 
			
		||||
    computed,
 | 
			
		||||
    defineComponent,
 | 
			
		||||
    nextTick,
 | 
			
		||||
    onMounted,
 | 
			
		||||
    onUnmounted,
 | 
			
		||||
    ref,
 | 
			
		||||
    shallowReactive,
 | 
			
		||||
@ -46,6 +47,7 @@ export interface TextContentProps
 | 
			
		||||
export type TextContentEmits = {
 | 
			
		||||
    typeEnd: () => void;
 | 
			
		||||
    typeStart: () => void;
 | 
			
		||||
    updateHeight: (height: number) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface TextContentExpose {
 | 
			
		||||
@ -89,7 +91,7 @@ const textContentOptions = {
 | 
			
		||||
        'width',
 | 
			
		||||
        'autoHeight'
 | 
			
		||||
    ],
 | 
			
		||||
    emits: ['typeEnd', 'typeStart']
 | 
			
		||||
    emits: ['typeEnd', 'typeStart', 'updateHeight']
 | 
			
		||||
} satisfies SetupComponentOptions<
 | 
			
		||||
    TextContentProps,
 | 
			
		||||
    TextContentEmits,
 | 
			
		||||
@ -144,10 +146,12 @@ export const TextContent = defineComponent<
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const updateLoc = () => {
 | 
			
		||||
        const height = getHeight();
 | 
			
		||||
        if (props.autoHeight) {
 | 
			
		||||
            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 });
 | 
			
		||||
@ -198,6 +202,8 @@ export const TextContent = defineComponent<
 | 
			
		||||
        emit('typeEnd');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onMounted(retype);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
        return (
 | 
			
		||||
            <sprite
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,9 @@ import {
 | 
			
		||||
import EventEmitter from 'eventemitter3';
 | 
			
		||||
import { isNil } from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
 | 
			
		||||
const SAFE_PAD = 1;
 | 
			
		||||
 | 
			
		||||
export const enum WordBreak {
 | 
			
		||||
    /** 不换行 */
 | 
			
		||||
    None,
 | 
			
		||||
@ -110,6 +113,8 @@ export interface ITextContentRenderable {
 | 
			
		||||
export interface ITextContentRenderObject {
 | 
			
		||||
    /** 每一行的高度 */
 | 
			
		||||
    lineHeights: number[];
 | 
			
		||||
    /** 每一行的宽度 */
 | 
			
		||||
    lineWidths: number[];
 | 
			
		||||
    /** 渲染数据 */
 | 
			
		||||
    data: ITextContentRenderable[];
 | 
			
		||||
}
 | 
			
		||||
@ -173,7 +178,8 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
 | 
			
		||||
    /** 渲染信息 */
 | 
			
		||||
    private renderObject: ITextContentRenderObject = {
 | 
			
		||||
        lineHeights: [],
 | 
			
		||||
        data: []
 | 
			
		||||
        data: [],
 | 
			
		||||
        lineWidths: []
 | 
			
		||||
    };
 | 
			
		||||
    /** 渲染信息 */
 | 
			
		||||
    private renderData: TyperRenderable[] = [];
 | 
			
		||||
@ -239,7 +245,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
 | 
			
		||||
    getHeight() {
 | 
			
		||||
        const heights = this.renderObject.lineHeights;
 | 
			
		||||
        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.dataLine = 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
        const renderable = this.renderObject.data[index];
 | 
			
		||||
        if (!renderable) return false;
 | 
			
		||||
@ -303,7 +322,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
 | 
			
		||||
 | 
			
		||||
                const data: TyperTextRenderable = {
 | 
			
		||||
                    type: TextContentType.Text,
 | 
			
		||||
                    x: this.x,
 | 
			
		||||
                    x: this.getDataX(line),
 | 
			
		||||
                    y: this.y,
 | 
			
		||||
                    text: renderable.text.slice(start, end),
 | 
			
		||||
                    font: renderable.font,
 | 
			
		||||
@ -331,7 +350,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
 | 
			
		||||
                }
 | 
			
		||||
                const data: TyperIconRenderable = {
 | 
			
		||||
                    type: TextContentType.Icon,
 | 
			
		||||
                    x: this.x,
 | 
			
		||||
                    x: this.getDataX(line),
 | 
			
		||||
                    y: this.y,
 | 
			
		||||
                    width: iconWidth,
 | 
			
		||||
                    height: iconWidth / aspect,
 | 
			
		||||
@ -537,6 +556,8 @@ export class TextContentParser {
 | 
			
		||||
    private lineHeight: number = 0;
 | 
			
		||||
    /** 每一行的行高 */
 | 
			
		||||
    private lineHeights: number[] = [];
 | 
			
		||||
    /** 每一行的宽度 */
 | 
			
		||||
    private lineWidths: number[] = [];
 | 
			
		||||
    /** 当前这一行已经有多长 */
 | 
			
		||||
    private lineWidth: number = 0;
 | 
			
		||||
    /** 这一行未计算部分的起始位置索引 */
 | 
			
		||||
@ -809,6 +830,7 @@ export class TextContentParser {
 | 
			
		||||
        this.nowRenderable = -1;
 | 
			
		||||
        this.lineHeight = 0;
 | 
			
		||||
        this.lineHeights = [];
 | 
			
		||||
        this.lineWidths = [];
 | 
			
		||||
        this.lineWidth = 0;
 | 
			
		||||
        this.lineStart = 0;
 | 
			
		||||
        this.guessGain = 1;
 | 
			
		||||
@ -967,6 +989,7 @@ export class TextContentParser {
 | 
			
		||||
                const index = this.bsLineWidth(maxWidth, this.nowRenderable);
 | 
			
		||||
                data.splitLines.push(this.wordBreak[index]);
 | 
			
		||||
                this.lineHeights.push(this.lineHeight);
 | 
			
		||||
                this.lineWidths.push(this.lineWidth);
 | 
			
		||||
                this.bsStart = index;
 | 
			
		||||
                const text = data.text.slice(
 | 
			
		||||
                    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 end = this.bsEnd;
 | 
			
		||||
        let height = 0;
 | 
			
		||||
        let width = 0;
 | 
			
		||||
 | 
			
		||||
        const data = this.renderable[index];
 | 
			
		||||
        const { wordBreak } = data;
 | 
			
		||||
@ -1005,6 +1029,7 @@ export class TextContentParser {
 | 
			
		||||
                if (height > this.lineHeight) {
 | 
			
		||||
                    this.lineHeight = height;
 | 
			
		||||
                }
 | 
			
		||||
                this.lineWidth = width;
 | 
			
		||||
                return start;
 | 
			
		||||
            }
 | 
			
		||||
            const text = data.text.slice(
 | 
			
		||||
@ -1012,10 +1037,12 @@ export class TextContentParser {
 | 
			
		||||
                wordBreak[mid] + 1
 | 
			
		||||
            );
 | 
			
		||||
            const metrics = ctx.measureText(text);
 | 
			
		||||
            width = metrics.width;
 | 
			
		||||
            height = this.getHeight(metrics);
 | 
			
		||||
            if (metrics.width > width) {
 | 
			
		||||
            if (width > maxWidth) {
 | 
			
		||||
                end = mid;
 | 
			
		||||
            } else if (metrics.width === width) {
 | 
			
		||||
            } else if (width === maxWidth) {
 | 
			
		||||
                this.lineWidth = width;
 | 
			
		||||
                if (height > this.lineHeight) {
 | 
			
		||||
                    this.lineHeight = height;
 | 
			
		||||
                }
 | 
			
		||||
@ -1064,6 +1091,7 @@ export class TextContentParser {
 | 
			
		||||
                    const index = this.bsLineWidth(maxWidth, pointer);
 | 
			
		||||
                    data.splitLines.push(this.wordBreak[index]);
 | 
			
		||||
                    this.lineHeights.push(this.lineHeight);
 | 
			
		||||
                    this.lineWidths.push(this.lineWidth);
 | 
			
		||||
                    this.bsStart = index;
 | 
			
		||||
                    const text = data.text.slice(this.wordBreak[index] + 1);
 | 
			
		||||
                    if (!isLast && text.length < guess / 4) {
 | 
			
		||||
@ -1089,9 +1117,9 @@ export class TextContentParser {
 | 
			
		||||
            let iconWidth = 0;
 | 
			
		||||
            if (aspect < 1) {
 | 
			
		||||
                // 这时候应该把高度限定在当前字体大小
 | 
			
		||||
                iconWidth = width * (this.status.fontSize / height);
 | 
			
		||||
                iconWidth = width * (data.fontSize / height);
 | 
			
		||||
            } else {
 | 
			
		||||
                iconWidth = this.status.fontSize;
 | 
			
		||||
                iconWidth = data.fontSize;
 | 
			
		||||
            }
 | 
			
		||||
            this.lineWidth += iconWidth;
 | 
			
		||||
            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 文字的宽度,到达这么宽之后换行
 | 
			
		||||
     */
 | 
			
		||||
    private splitLines(width: number): ITextContentRenderObject {
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
@ -1182,11 +1266,14 @@ export class TextContentParser {
 | 
			
		||||
            this.checkRestLine(width, guess, i);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.checkLastSize();
 | 
			
		||||
        this.lineHeights.push(this.lineHeight);
 | 
			
		||||
        this.lineWidths.push(this.lineWidth);
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            lineHeights: this.lineHeights,
 | 
			
		||||
            data: this.renderable
 | 
			
		||||
            data: this.renderable,
 | 
			
		||||
            lineWidths: this.lineWidths
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user