Compare commits

...

3 Commits

Author SHA1 Message Date
e232a469c8 feat: Background 组件 2025-02-27 16:38:33 +08:00
f26c047457 chore: move selection to misc.tsx 2025-02-27 16:07:22 +08:00
24ee1613ed feat: 滚动文字 2025-02-27 16:03:14 +08:00
9 changed files with 399 additions and 119 deletions

View File

@ -64,22 +64,24 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
logger.warn(33); logger.warn(33);
return; return;
} }
const w = Math.max(width, 1);
const h = Math.max(height, 1);
let ratio = this.highResolution ? devicePixelRatio : 1; let ratio = this.highResolution ? devicePixelRatio : 1;
const scale = core.domStyle.scale; const scale = core.domStyle.scale;
if (this.autoScale) { if (this.autoScale) {
ratio *= scale; ratio *= scale;
} }
this.scale = ratio; this.scale = ratio;
this.canvas.width = width * ratio; this.canvas.width = w * ratio;
this.canvas.height = height * ratio; this.canvas.height = h * ratio;
this.width = width; this.width = w;
this.height = height; this.height = height;
this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.scale(ratio, ratio); this.ctx.scale(ratio, ratio);
this.ctx.imageSmoothingEnabled = this.antiAliasing; this.ctx.imageSmoothingEnabled = this.antiAliasing;
if (this.canvas.isConnected) { if (this.canvas.isConnected) {
this.canvas.style.width = `${width * scale}px`; this.canvas.style.width = `${w * scale}px`;
this.canvas.style.height = `${height * scale}px`; this.canvas.style.height = `${h * scale}px`;
} }
} }

View File

@ -0,0 +1,18 @@
import { DefaultProps } from '@/core/render';
import { defineComponent } from 'vue';
export interface ConfirmBoxProps extends DefaultProps {
text: string;
yesText?: string;
noText?: string;
winskin?: string;
}
export interface ConfirmBoxEmits {
onYes: () => void;
onNo: () => void;
}
export const ConfirmBox = defineComponent(() => {
return () => <container></container>;
});

View File

@ -1,7 +1,17 @@
import { DefaultProps, ElementLocator, PathProps, Sprite } from '@/core/render'; import {
DefaultProps,
ElementLocator,
onTick,
PathProps,
Sprite
} from '@/core/render';
import { computed, defineComponent, ref, watch } from 'vue'; import { computed, defineComponent, ref, watch } from 'vue';
import { SetupComponentOptions } from './types'; import { SetupComponentOptions } from './types';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { TextboxProps, TextContent } from './textbox';
import { Scroll, ScrollExpose, ScrollProps } from './scroll';
import { transitioned } from '../use';
import { hyper } from 'mutate-animate';
interface ProgressProps extends DefaultProps { interface ProgressProps extends DefaultProps {
/** 进度条的位置 */ /** 进度条的位置 */
@ -20,6 +30,16 @@ const progressProps = {
props: ['loc', 'progress', 'success', 'background'] props: ['loc', 'progress', 'success', 'background']
} satisfies SetupComponentOptions<ProgressProps>; } satisfies SetupComponentOptions<ProgressProps>;
/**
* {@link ProgressProps}
* ```tsx
* // 定义进度
* const progress = ref(0);
*
* // 显示进度条
* <Progress loc={[12, 12, 120, 8]} progress={progress.value} />
* ```
*/
export const Progress = defineComponent<ProgressProps>(props => { export const Progress = defineComponent<ProgressProps>(props => {
const element = ref<Sprite>(); const element = ref<Sprite>();
@ -55,7 +75,7 @@ export const Progress = defineComponent<ProgressProps>(props => {
}, progressProps); }, progressProps);
export interface ArrowProps extends PathProps { export interface ArrowProps extends PathProps {
/** 箭头的四个坐标 */ /** 箭头的起始和终点坐标,前两个是起始坐标,后两个是终点坐标 */
arrow: [number, number, number, number]; arrow: [number, number, number, number];
/** 箭头的头部大小 */ /** 箭头的头部大小 */
head?: number; head?: number;
@ -67,6 +87,13 @@ const arrowProps = {
props: ['arrow', 'head', 'color'] props: ['arrow', 'head', 'color']
} satisfies SetupComponentOptions<ArrowProps>; } satisfies SetupComponentOptions<ArrowProps>;
/**
* {@link ArrowProps}
* ```tsx
* // 从 (12, 12) 到 (48, 48) 的箭头
* <Arrow arrow={[12, 12, 48, 48]} />
* ```
*/
export const Arrow = defineComponent<ArrowProps>(props => { export const Arrow = defineComponent<ArrowProps>(props => {
const loc = computed<ElementLocator>(() => { const loc = computed<ElementLocator>(() => {
const [x1, y1, x2, y2] = props.arrow; const [x1, y1, x2, y2] = props.arrow;
@ -107,3 +134,261 @@ export const Arrow = defineComponent<ArrowProps>(props => {
/> />
); );
}, arrowProps); }, arrowProps);
export interface ScrollTextProps extends TextboxProps, ScrollProps {
/** 自动滚动的速度,每秒多少像素 */
speed: number;
/** 文字的最大宽度 */
width: number;
/** 自动滚动组件的定位 */
loc: ElementLocator;
/** 文字滚动入元素之前要先滚动多少像素默认16像素 */
pad?: number;
}
export type ScrollTextEmits = {
/**
*
*/
scrollEnd: () => void;
};
export interface ScrollTextExpose {
/**
*
*/
pause(): void;
/**
*
*/
resume(): void;
/**
*
*/
setSpeed(speed: number): void;
/**
*
*/
rescroll(): void;
}
const scrollProps = {
props: ['speed', 'loc', 'pad', 'width'],
emits: ['scrollEnd']
} satisfies SetupComponentOptions<
ScrollTextProps,
ScrollTextEmits,
keyof ScrollTextEmits
>;
/**
* staff {@link ScrollTextProps}
* {@link ScrollTextEmits} {@link ScrollTextExpose}
* ```tsx
* // 用于接受函数接口
* const scroll = ref<ScrollTextExpose>();
* // 显示的文字
* const text = '滚动文字'.repeat(100);
*
* onMounted(() => {
* // 设置为每秒滚动 100 像素
* scroll.value?.setSpeed(100);
* // 暂停滚动
* scroll.value?.pause();
* });
*
* // 显示滚动文字,每秒滚动 50 像素
* <ScrollText ref={scroll} text={text} speed={50} loc={[0, 0, 180, 100]} width={180} />
* ```
*/
export const ScrollText = defineComponent<
ScrollTextProps,
ScrollTextEmits,
keyof ScrollTextEmits
>((props, { emit, expose, attrs }) => {
const scroll = ref<ScrollExpose>();
const speed = ref(props.speed);
const eleHeight = computed(() => props.loc[3] ?? props.height ?? 200);
const pad = computed(() => props.pad ?? 16);
let lastFixedTime = Date.now();
let lastFixedPos = 0;
let paused = false;
let nowScroll = 0;
onTick(() => {
if (paused || !scroll.value) return;
const now = Date.now();
const dt = now - lastFixedTime;
nowScroll = (dt / 1000) * speed.value + lastFixedPos;
scroll.value.scrollTo(nowScroll, 0);
if (nowScroll >= scroll.value.getScrollLength()) {
emit('scrollEnd');
paused = true;
}
});
const pause = () => {
paused = true;
};
const resume = () => {
paused = false;
lastFixedPos = nowScroll;
lastFixedTime = Date.now();
};
const setSpeed = (value: number) => {
lastFixedPos = nowScroll;
lastFixedTime = Date.now();
speed.value = value;
};
const rescroll = () => {
nowScroll = 0;
lastFixedTime = Date.now();
lastFixedPos = 0;
};
expose<ScrollTextExpose>({ pause, resume, setSpeed, rescroll });
return () => (
<Scroll
ref={scroll}
loc={props.loc}
padEnd={eleHeight.value + pad.value}
noscroll
>
<TextContent
{...attrs}
width={props.width - 16}
loc={[8, eleHeight.value + pad.value]}
autoHeight
/>
</Scroll>
);
}, scrollProps);
export interface SelectionProps extends DefaultProps {
loc: ElementLocator;
color?: CanvasStyle;
border?: CanvasStyle;
winskin?: ImageIds;
/** 选择图标的不透明度范围 */
alphaRange?: [number, number];
}
const selectionProps = {
props: ['loc', 'color', 'border', 'winskin', 'alphaRange']
} satisfies SetupComponentOptions<SelectionProps>;
/**
* 2.x drawUIEventSelector {@link SelectionProps}
* ```tsx
* // 使用 winskin.png 作为选择光标,光标动画的不透明度范围是 [0.3, 0.8]
* <Selection loc={[24, 24, 80, 16]} winskin="winskin.png" alphaRange={[0.3, 0.8]} />
* // 使用指定的填充和边框颜色作为选择光标
* <Selection loc={[24, 24, 80, 16]} color="#ddd" border="gold" />
* ```
*/
export const Selection = defineComponent<SelectionProps>(props => {
const minAlpha = computed(() => props.alphaRange?.[0] ?? 0.25);
const maxAlpha = computed(() => props.alphaRange?.[1] ?? 0.55);
const alpha = transitioned(minAlpha.value, 2000, hyper('sin', 'in-out'))!;
const isWinskin = computed(() => !!props.winskin);
const winskinImage = computed(() =>
isWinskin.value ? core.material.images.images[props.winskin!] : null
);
const fixedLoc = computed<ElementLocator>(() => {
const [x = 0, y = 0, width = 200, height = 200] = props.loc;
return [x + 1, y + 1, width - 2, height - 2];
});
const renderWinskin = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx;
const image = winskinImage.value;
if (!image) return;
const [, , width = 200, height = 200] = props.loc;
// 背景
ctx.drawImage(image, 130, 66, 28, 28, 2, 2, width - 4, height - 4);
// 四个角
ctx.drawImage(image, 128, 64, 2, 2, 0, 0, 2, 2);
ctx.drawImage(image, 158, 64, 2, 2, width - 2, 0, 2, 2);
ctx.drawImage(image, 128, 94, 2, 2, 0, height - 2, 2, 2);
ctx.drawImage(image, 158, 94, 2, 2, width - 2, height - 2, 2, 2);
// 四条边
ctx.drawImage(image, 130, 64, 28, 2, 2, 0, width - 4, 2);
ctx.drawImage(image, 130, 94, 28, 2, 2, height - 2, width - 4, 2);
ctx.drawImage(image, 128, 66, 2, 28, 0, 2, 2, height - 4);
ctx.drawImage(image, 158, 66, 2, 28, width - 2, 2, 2, height - 4);
};
onTick(() => {
if (alpha.value === maxAlpha.value) {
alpha.set(minAlpha.value);
}
if (alpha.value === minAlpha.value) {
alpha.set(maxAlpha.value);
}
});
return () =>
isWinskin.value ? (
<sprite
loc={props.loc}
render={renderWinskin}
alpha={alpha.ref.value}
noanti
/>
) : (
<g-rectr
loc={fixedLoc.value}
circle={[4]}
alpha={alpha.ref.value}
fill
stroke
fillStyle={props.color}
strokeStyle={props.border}
lineWidth={1}
/>
);
}, selectionProps);
export interface BackgroundProps extends DefaultProps {
loc: ElementLocator;
winskin?: ImageIds;
color?: CanvasStyle;
border?: CanvasStyle;
}
const backgroundProps = {
props: ['loc', 'winskin', 'color', 'border']
} satisfies SetupComponentOptions<BackgroundProps>;
export const Background = defineComponent<BackgroundProps>(props => {
const isWinskin = computed(() => !!props.winskin);
const fixedLoc = computed<ElementLocator>(() => {
const [x = 0, y = 0, width = 200, height = 200] = props.loc;
return [x + 2, y + 2, width - 4, height - 4];
});
return () =>
isWinskin.value ? (
<winskin image={props.winskin!} loc={props.loc} noanti />
) : (
<g-rectr
loc={fixedLoc.value}
fillStyle={props.color}
strokeStyle={props.border}
fill
stroke
lineWidth={2}
circle={[4]}
/>
);
}, backgroundProps);

View File

@ -37,6 +37,11 @@ export interface ScrollExpose {
* @param time * @param time
*/ */
scrollTo(y: number, time?: number): void; scrollTo(y: number, time?: number): void;
/**
*
*/
getScrollLength(): number;
} }
export interface ScrollProps extends DefaultProps { export interface ScrollProps extends DefaultProps {
@ -47,7 +52,7 @@ export interface ScrollProps extends DefaultProps {
* *
* *
*/ */
pad?: number; padEnd?: number;
} }
type ScrollSlots = SlotsType<{ type ScrollSlots = SlotsType<{
@ -55,7 +60,7 @@ type ScrollSlots = SlotsType<{
}>; }>;
const scrollProps = { const scrollProps = {
props: ['hor', 'noscroll', 'loc', 'pad'] props: ['hor', 'noscroll', 'loc', 'padEnd']
} satisfies SetupComponentOptions<ScrollProps, {}, string, ScrollSlots>; } satisfies SetupComponentOptions<ScrollProps, {}, string, ScrollSlots>;
/** 滚动条图示的最短长度 */ /** 滚动条图示的最短长度 */
@ -112,6 +117,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
const scrollColor = computed( const scrollColor = computed(
() => `rgba(${SCROLL_COLOR},${scrollAlpha.ref.value ?? 0.5})` () => `rgba(${SCROLL_COLOR},${scrollAlpha.ref.value ?? 0.5})`
); );
const padEnd = computed(() => props.padEnd ?? 0);
watch(scrollColor, () => { watch(scrollColor, () => {
scroll.value?.update(); scroll.value?.update();
@ -145,7 +151,6 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
if (contentPos !== contentTarget) { if (contentPos !== contentTarget) {
contentPos = transition.value.showScroll; contentPos = transition.value.showScroll;
checkAllItem(); checkAllItem();
updatePosition();
content.value?.update(); content.value?.update();
} }
}); });
@ -211,7 +216,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
*/ */
const onTransform = (item: RenderItem) => { const onTransform = (item: RenderItem) => {
const rect = item.getBoundingRect(); const rect = item.getBoundingRect();
const pad = props.pad ?? 0; const pad = props.padEnd ?? 0;
if (item.parent === content.value) { if (item.parent === content.value) {
if (direction.value === ScrollDirection.Horizontal) { if (direction.value === ScrollDirection.Horizontal) {
if (rect.right > maxLength - pad) { if (rect.right > maxLength - pad) {
@ -291,7 +296,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
} }
checkItem(v); checkItem(v);
}); });
maxLength = Math.max(max + (props.pad ?? 0), 10); maxLength = Math.max(max + padEnd.value, 10);
updatePosition(); updatePosition();
scroll.value?.update(); scroll.value?.update();
}; };
@ -504,10 +509,19 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
transition.ticker.destroy(); transition.ticker.destroy();
}); });
//#region expose 函数
const getScrollLength = () => {
return maxLength - height.value;
};
expose<ScrollExpose>({ expose<ScrollExpose>({
scrollTo scrollTo,
getScrollLength
}); });
//#endregion
return () => { return () => {
return ( return (
<container loc={props.loc} onWheel={wheel}> <container loc={props.loc} onWheel={wheel}>

View File

@ -1,83 +0,0 @@
import { DefaultProps, ElementLocator, onTick } from '@/core/render';
import { computed, defineComponent } from 'vue';
import { SetupComponentOptions } from './types';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { transitioned } from '../use';
import { hyper } from 'mutate-animate';
export interface SelectionProps extends DefaultProps {
loc: ElementLocator;
color?: CanvasStyle;
border?: CanvasStyle;
winskin?: ImageIds;
/** 选择图标的不透明度范围 */
alphaRange?: [number, number];
}
const selectionProps = {
props: ['loc', 'color', 'border', 'winskin', 'alphaRange']
} satisfies SetupComponentOptions<SelectionProps>;
export const Selection = defineComponent<SelectionProps>(props => {
const minAlpha = computed(() => props.alphaRange?.[0] ?? 0.25);
const maxAlpha = computed(() => props.alphaRange?.[1] ?? 0.55);
const alpha = transitioned(minAlpha.value, 2000, hyper('sin', 'in-out'))!;
const isWinskin = computed(() => !!props.winskin);
const winskinImage = computed(() =>
isWinskin.value ? core.material.images.images[props.winskin!] : null
);
const fixedLoc = computed<ElementLocator>(() => {
const [x = 0, y = 0, width = 200, height = 200] = props.loc;
return [x + 1, y + 1, width - 2, height - 2];
});
const renderWinskin = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx;
const image = winskinImage.value;
if (!image) return;
const [, , width = 200, height = 200] = props.loc;
// 背景
ctx.drawImage(image, 130, 66, 28, 28, 2, 2, width - 4, height - 4);
// 四个角
ctx.drawImage(image, 128, 64, 2, 2, 0, 0, 2, 2);
ctx.drawImage(image, 158, 64, 2, 2, width - 2, 0, 2, 2);
ctx.drawImage(image, 128, 94, 2, 2, 0, height - 2, 2, 2);
ctx.drawImage(image, 158, 94, 2, 2, width - 2, height - 2, 2, 2);
// 四条边
ctx.drawImage(image, 130, 64, 28, 2, 2, 0, width - 4, 2);
ctx.drawImage(image, 130, 94, 28, 2, 2, height - 2, width - 4, 2);
ctx.drawImage(image, 128, 66, 2, 28, 0, 2, 2, height - 4);
ctx.drawImage(image, 158, 66, 2, 28, width - 2, 2, 2, height - 4);
};
onTick(() => {
if (alpha.value === maxAlpha.value) {
alpha.set(minAlpha.value);
}
if (alpha.value === minAlpha.value) {
alpha.set(maxAlpha.value);
}
});
return () =>
isWinskin.value ? (
<sprite
loc={props.loc}
render={renderWinskin}
alpha={alpha.ref.value}
noanti
/>
) : (
<g-rectr
loc={fixedLoc.value}
circle={[4]}
alpha={alpha.ref.value}
fill
stroke
fillStyle={props.color}
strokeStyle={props.border}
lineWidth={1}
/>
);
}, selectionProps);

View File

@ -26,6 +26,7 @@ import {
WordBreak, WordBreak,
TextAlign TextAlign
} from './textboxTyper'; } from './textboxTyper';
import { ElementLocator } from '@/core/render';
export interface TextContentProps export interface TextContentProps
extends DefaultProps, extends DefaultProps,
@ -36,6 +37,10 @@ export interface TextContentProps
fill?: boolean; fill?: boolean;
/** 是否描边 */ /** 是否描边 */
stroke?: boolean; stroke?: boolean;
/** 是否自适应高度 */
autoHeight?: boolean;
/** 文字的最大宽度 */
width: number;
} }
export type TextContentEmits = { export type TextContentEmits = {
@ -53,6 +58,11 @@ export interface TextContentExpose {
* *
*/ */
showAll(): void; showAll(): void;
/**
* TextContent
*/
getHeight(): number;
} }
const textContentOptions = { const textContentOptions = {
@ -76,7 +86,8 @@ const textContentOptions = {
'strokeWidth', 'strokeWidth',
'stroke', 'stroke',
'loc', 'loc',
'width' 'width',
'autoHeight'
], ],
emits: ['typeEnd', 'typeStart'] emits: ['typeEnd', 'typeStart']
} satisfies SetupComponentOptions< } satisfies SetupComponentOptions<
@ -90,8 +101,11 @@ export const TextContent = defineComponent<
TextContentEmits, TextContentEmits,
keyof TextContentEmits keyof TextContentEmits
>((props, { emit, expose }) => { >((props, { emit, expose }) => {
const width = computed(() => props.width ?? props.loc?.[2] ?? 200); const loc = ref<ElementLocator>(
if (width.value < 0) { (props.loc?.slice() as ElementLocator) ?? []
);
if (props.width < 0) {
logger.warn(41, String(props.width)); logger.warn(41, String(props.width));
} }
@ -112,6 +126,7 @@ export const TextContent = defineComponent<
typer.setText(props.text ?? ''); typer.setText(props.text ?? '');
typer.type(); typer.type();
needUpdate = false; needUpdate = false;
updateLoc();
}); });
}; };
@ -119,12 +134,23 @@ export const TextContent = defineComponent<
typer.typeAll(); typer.typeAll();
}; };
watch(props, value => { watch(props, () => {
typer.setConfig(value); typer.setConfig(props);
retype(); retype();
}); });
expose({ retype, showAll }); const getHeight = () => {
return typer.getHeight();
};
const updateLoc = () => {
if (props.autoHeight) {
const [x = 0, y = 0, width = 200, , ax = 0, ay = 0] = loc.value;
loc.value = [x, y, width, getHeight(), ax, ay];
}
};
expose<TextContentExpose>({ retype, showAll, getHeight });
const spriteElement = shallowRef<Sprite>(); const spriteElement = shallowRef<Sprite>();
const renderContent = (canvas: MotaOffscreenCanvas2D) => { const renderContent = (canvas: MotaOffscreenCanvas2D) => {
@ -175,7 +201,7 @@ export const TextContent = defineComponent<
return () => { return () => {
return ( return (
<sprite <sprite
loc={props.loc} loc={loc.value}
ref={spriteElement} ref={spriteElement}
render={renderContent} render={renderContent}
></sprite> ></sprite>
@ -200,6 +226,8 @@ export interface TextboxProps extends TextContentProps, DefaultProps {
titleStroke?: CanvasStyle; titleStroke?: CanvasStyle;
/** 标题文字与边框间的距离默认为4 */ /** 标题文字与边框间的距离默认为4 */
titlePadding?: number; titlePadding?: number;
/** 最大宽度 */
width: number;
} }
export interface TextboxExpose { export interface TextboxExpose {
@ -257,8 +285,8 @@ export const Textbox = defineComponent<
keyof TextboxEmits, keyof TextboxEmits,
TextboxSlots TextboxSlots
>((props, { slots, expose }) => { >((props, { slots, expose }) => {
const contentData = shallowReactive<TextContentProps>({}); const contentData = shallowReactive<TextContentProps>({ width: 200 });
const data = shallowReactive<TextboxProps>({}); const data = shallowReactive<TextboxProps>({ width: 200 });
const setContentData = () => { const setContentData = () => {
contentData.breakChars = props.breakChars ?? ''; contentData.breakChars = props.breakChars ?? '';

View File

@ -124,6 +124,8 @@ export interface TyperTextRenderable {
strokeStyle: CanvasStyle; strokeStyle: CanvasStyle;
/** 文字画到哪个索引 */ /** 文字画到哪个索引 */
pointer: number; pointer: number;
/** 这段文字的总高度 */
height: number;
} }
export interface TyperIconRenderable { export interface TyperIconRenderable {
@ -231,6 +233,15 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
onTick(() => this.tick()); onTick(() => this.tick());
} }
/**
*
*/
getHeight() {
const heights = this.renderObject.lineHeights;
const lines = heights.reduce((prev, curr) => prev + curr, 0);
return lines + this.config.lineHeight * heights.length;
}
/** /**
* *
* @param config * @param config
@ -285,19 +296,21 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
if (line < 0 || line > renderable.splitLines.length) { if (line < 0 || line > renderable.splitLines.length) {
return false; return false;
} }
const start = renderable.splitLines[line - 1] ?? -1; const start = renderable.splitLines[line - 1] ?? 0;
const end = const end =
renderable.splitLines[line] ?? renderable.text.length - 1; renderable.splitLines[line] ?? renderable.text.length;
const lineHeight = this.renderObject.lineHeights[this.nowLine];
const data: TyperTextRenderable = { const data: TyperTextRenderable = {
type: TextContentType.Text, type: TextContentType.Text,
x: this.x, x: this.x,
y: this.y, y: this.y,
text: renderable.text.slice(start + 1, end + 1), text: renderable.text.slice(start, end),
font: renderable.font, font: renderable.font,
fillStyle: renderable.fillStyle, fillStyle: renderable.fillStyle,
strokeStyle: this.config.strokeStyle, strokeStyle: this.config.strokeStyle,
pointer: 0 pointer: 0,
height: lineHeight + this.config.lineHeight
}; };
this.processingData = data; this.processingData = data;
this.renderData.push(data); this.renderData.push(data);
@ -926,7 +939,7 @@ export class TextContentParser {
const rest = width - this.lineWidth; const rest = width - this.lineWidth;
const guessRest = guess * (rest / width) * this.guessGain; const guessRest = guess * (rest / width) * this.guessGain;
const length = pointer - this.lineStart + 1; const length = pointer - this.lineStart + 1;
if (length < guessRest) { if (length <= guessRest) {
return false; return false;
} }
this.guessGain = 1; this.guessGain = 1;
@ -941,7 +954,7 @@ export class TextContentParser {
if (height > this.lineHeight) { if (height > this.lineHeight) {
this.lineHeight = height; this.lineHeight = height;
} }
if (metrics.width < rest) { if (metrics.width <= rest) {
// 实际宽度小于剩余宽度时,将猜测增益乘以剩余总宽度与当前宽度的比值的若干倍 // 实际宽度小于剩余宽度时,将猜测增益乘以剩余总宽度与当前宽度的比值的若干倍
this.guessGain *= (rest / metrics.width) * (1.1 + 1 / length); this.guessGain *= (rest / metrics.width) * (1.1 + 1 / length);
this.bsStart = breakIndex; this.bsStart = breakIndex;
@ -956,7 +969,7 @@ export class TextContentParser {
this.lineHeights.push(this.lineHeight); this.lineHeights.push(this.lineHeight);
this.bsStart = index; this.bsStart = index;
const text = data.text.slice( const text = data.text.slice(
this.wordBreak[index], this.wordBreak[index] + 1,
pointer + 1 pointer + 1
); );
if (text.length < guessRest / 4) { if (text.length < guessRest / 4) {
@ -995,7 +1008,7 @@ export class TextContentParser {
return start; return start;
} }
const text = data.text.slice( const text = data.text.slice(
wordBreak[this.bsStart], wordBreak[this.bsStart] + 1,
wordBreak[mid] + 1 wordBreak[mid] + 1
); );
const metrics = ctx.measureText(text); const metrics = ctx.measureText(text);
@ -1028,7 +1041,7 @@ export class TextContentParser {
const wordBreak = data.wordBreak; const wordBreak = data.wordBreak;
const lastLine = data.splitLines.at(-1); const lastLine = data.splitLines.at(-1);
const lastIndex = isNil(lastLine) ? 0 : lastLine; const lastIndex = isNil(lastLine) ? 0 : lastLine;
const restText = data.text.slice(lastIndex); const restText = data.text.slice(lastIndex + 1);
const ctx = this.testCanvas.ctx; const ctx = this.testCanvas.ctx;
ctx.font = data.font; ctx.font = data.font;
const metrics = ctx.measureText(restText); const metrics = ctx.measureText(restText);
@ -1052,7 +1065,7 @@ export class TextContentParser {
data.splitLines.push(this.wordBreak[index]); data.splitLines.push(this.wordBreak[index]);
this.lineHeights.push(this.lineHeight); this.lineHeights.push(this.lineHeight);
this.bsStart = index; this.bsStart = index;
const text = data.text.slice(this.wordBreak[index]); const text = data.text.slice(this.wordBreak[index] + 1);
if (!isLast && text.length < guess / 4) { if (!isLast && text.length < guess / 4) {
// 如果剩余文字很少,几乎不可能会单独成一行时,直接结束循环 // 如果剩余文字很少,几乎不可能会单独成一行时,直接结束循环
this.lastBreakIndex = index; this.lastBreakIndex = index;
@ -1169,6 +1182,8 @@ export class TextContentParser {
this.checkRestLine(width, guess, i); this.checkRestLine(width, guess, i);
} }
this.lineHeights.push(this.lineHeight);
return { return {
lineHeights: this.lineHeights, lineHeights: this.lineHeights,
data: this.renderable data: this.renderable

View File

@ -66,7 +66,8 @@ const MainScene = defineComponent(() => {
titleFont: '700 20px normal', titleFont: '700 20px normal',
winskin: 'winskin2.png', winskin: 'winskin2.png',
interval: 100, interval: 100,
lineHeight: 4 lineHeight: 4,
width: 480
}; };
const map = ref<LayerGroup>(); const map = ref<LayerGroup>();

View File

@ -247,7 +247,7 @@ export const NumpadToolbar = defineComponent<
const altAlpha = transitioned(0, 100, linear())!; const altAlpha = transitioned(0, 100, linear())!;
const ctrlColor = computed( const ctrlColor = computed(
() => `rgba(255,255,255,${ctrlAlpha.ref.value})` () => `rgba(221,221,221,${ctrlAlpha.ref.value})`
); );
const ctrlTextColor = computed(() => { const ctrlTextColor = computed(() => {
const rgb = Math.floor(255 - ctrlAlpha.ref.value * 255); const rgb = Math.floor(255 - ctrlAlpha.ref.value * 255);
@ -255,14 +255,14 @@ export const NumpadToolbar = defineComponent<
}); });
const shiftColor = computed( const shiftColor = computed(
() => `rgba(255,255,255,${shiftAlpha.ref.value})` () => `rgba(221,221,221,${shiftAlpha.ref.value})`
); );
const shiftTextColor = computed(() => { const shiftTextColor = computed(() => {
const rgb = Math.floor(255 - shiftAlpha.ref.value * 255); const rgb = Math.floor(255 - shiftAlpha.ref.value * 255);
return `rgba(${rgb},${rgb},${rgb},1)`; return `rgba(${rgb},${rgb},${rgb},1)`;
}); });
const altColor = computed(() => `rgba(255,255,255,${altAlpha.ref.value})`); const altColor = computed(() => `rgba(221,221,221,${altAlpha.ref.value})`);
const altTextColor = computed(() => { const altTextColor = computed(() => {
const rgb = Math.floor(255 - altAlpha.ref.value * 255); const rgb = Math.floor(255 - altAlpha.ref.value * 255);
return `rgba(${rgb},${rgb},${rgb},1)`; return `rgba(${rgb},${rgb},${rgb},1)`;