feat: 楼层选择组件 & fix: 滚动条交互错位

This commit is contained in:
unanmed 2025-08-18 18:14:08 +08:00
parent 936c909451
commit b7c7cdeca0
10 changed files with 1209 additions and 798 deletions

View File

@ -75,7 +75,7 @@
"markdown-it-mathjax3": "^4.3.2",
"mermaid": "^11.5.0",
"postcss-preset-env": "^9.6.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"rollup": "^3.29.5",
"terser": "^5.39.0",
"tsx": "^4.19.3",

View File

@ -0,0 +1,202 @@
import { DefaultProps } from '@motajs/render-vue';
import { SetupComponentOptions } from '@motajs/system-ui';
import { clamp, isNil } from 'lodash-es';
import { computed, defineComponent, ref, watch } from 'vue';
import { Scroll, ScrollExpose } from './scroll';
import { Font } from '@motajs/render-style';
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
export interface FloorSelectorProps extends DefaultProps {
floors: FloorIds[];
now?: number;
}
export type FloorSelectorEmits = {
/**
*
*/
close: () => void;
/**
*
* @param floor
* @param floorId id
*/
update: (floor: number, floorId: FloorIds) => void;
'update:now': (value: number) => void;
};
const floorSelectorProps = {
props: ['floors', 'now'],
emits: ['close', 'update', 'update:now']
} satisfies SetupComponentOptions<
FloorSelectorProps,
FloorSelectorEmits,
keyof FloorSelectorEmits
>;
export const FloorSelector = defineComponent<
FloorSelectorProps,
FloorSelectorEmits,
keyof FloorSelectorEmits
>((props, { emit }) => {
const listFont = new Font(Font.defaultFamily, 12);
const now = ref(props.now ?? 0);
const selList = ref(0);
const scrollRef = ref<ScrollExpose>();
const floorId = computed(() => props.floors[now.value]);
const floorName = computed(() => core.floors[floorId.value].title);
watch(
() => props.now,
value => {
if (!isNil(value)) now.value = value;
}
);
let gradient: CanvasGradient | null = null;
const getGradient = (ctx: CanvasRenderingContext2D) => {
if (gradient) return gradient;
gradient = ctx.createLinearGradient(0, 0, 0, 200);
gradient.addColorStop(0, 'rgba(255,255,255,0)');
gradient.addColorStop(0.2, 'rgba(255,255,255,1)');
gradient.addColorStop(0.8, 'rgba(255,255,255,1)');
gradient.addColorStop(1, 'rgba(255,255,255,0)');
return gradient;
};
const renderMask = (canvas: MotaOffscreenCanvas2D) => {
const { ctx } = canvas;
const gradient = getGradient(ctx);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 144, 200);
};
const changeTo = (index: number) => {
const res = clamp(index, 0, props.floors.length - 1);
now.value = res;
selList.value = res;
const y = res * 24;
scrollRef.value?.scrollTo(y, 500);
emit('update', now.value, floorId.value);
emit('update:now', now.value);
};
const changeFloor = (delta: number) => {
changeTo(now.value + delta);
};
const enterList = (index: number) => {
selList.value = index;
};
const close = () => {
emit('close');
};
return () => (
<container>
<text text={floorName.value} loc={[90, 24]} anc={[0.5, 0.5]} />
<g-line line={[48, 40, 132, 40]} lineWidth={1} />
<g-line line={[48, 440, 132, 440]} lineWidth={1} />
<text
text="退出"
loc={[90, 456]}
anc={[0.5, 0.5]}
cursor="pointer"
onClick={close}
/>
<text
text="「 上移十层 」"
loc={[90, 70]}
anc={[0.5, 0.5]}
cursor="pointer"
onClick={() => changeFloor(-10)}
/>
<text
text="「 上移一层 」"
loc={[90, 110]}
anc={[0.5, 0.5]}
cursor="pointer"
onClick={() => changeFloor(-1)}
/>
<text
text="「 下移一层 」"
loc={[90, 370]}
anc={[0.5, 0.5]}
cursor="pointer"
onClick={() => changeFloor(1)}
/>
<text
text="「 下移十层 」"
loc={[90, 410]}
anc={[0.5, 0.5]}
cursor="pointer"
onClick={() => changeFloor(10)}
/>
<container loc={[0, 140, 144, 200]}>
<Scroll
ref={scrollRef}
loc={[0, 0, 144, 200]}
noscroll
zIndex={10}
padEnd={88}
>
{props.floors.map((v, i) => {
const floor = core.floors[v];
const highlight =
now.value === i || selList.value === i;
const color = highlight ? '#fff' : '#aaa';
const fill = highlight ? '#fff' : '#000';
return (
<container
nocache
loc={[0, i * 24 + 88, 144, 24]}
key={v}
>
<text
cursor="pointer"
text={floor.title}
loc={[114, 12]}
anc={[1, 0.5]}
font={listFont}
fillStyle={color}
onEnter={() => enterList(i)}
onLeave={() => enterList(now.value)}
onClick={() => changeTo(i)}
/>
<g-circle
stroke
fill
lineWidth={1}
circle={[130, 12, 3]}
strokeStyle={color}
fillStyle={now.value === i ? fill : '#000'}
/>
</container>
);
})}
</Scroll>
<g-line
line={[130, 0, 130, 200]}
zIndex={5}
lineWidth={1}
strokeStyle="#aaa"
/>
<sprite
zIndex={20}
loc={[0, 0, 144, 200]}
nocache
noevent
render={renderMask}
composite="destination-in"
/>
</container>
</container>
);
}, floorSelectorProps);

View File

@ -20,7 +20,12 @@ import {
MotaOffscreenCanvas2D,
IActionEvent,
IWheelEvent,
MouseType
MouseType,
EventProgress,
ActionEventMap,
ContainerCustom,
ActionType,
CustomContainerPropagateOrigin
} from '@motajs/render';
import { hyper, linear, Transition } from 'mutate-animate';
import { clamp } from 'lodash-es';
@ -355,6 +360,23 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
//#region 事件监听
const customPropagate = <T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T],
_: ContainerCustom,
origin: CustomContainerPropagateOrigin
) => {
if (progress === EventProgress.Capture) {
if (direction.value === ScrollDirection.Horizontal) {
event.offsetX += contentPos;
} else {
event.offsetY += contentPos;
}
}
origin(type, progress, event);
};
const wheelScroll = (delta: number, max: number) => {
const sign = Math.sign(delta);
const dx = Math.abs(delta);
@ -532,6 +554,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
ref={content}
onDown={down}
render={renderContent}
propagate={customPropagate}
zIndex={0}
>
{slots.default?.()}

View File

@ -9,8 +9,8 @@ import { SetupComponentOptions } from '@motajs/system-ui';
export interface ThumbnailProps extends SpriteProps {
loc: ElementLocator;
padStyle: CanvasStyle;
floorId: FloorIds;
padStyle?: CanvasStyle;
map?: Block[];
hero?: HeroStatus;
// configs
@ -65,7 +65,7 @@ export const Thumbnail = defineComponent<ThumbnailProps>(props => {
options.centerY = hero.loc.y;
}
ctx.save();
ctx.fillStyle = props.padStyle;
ctx.fillStyle = props.padStyle ?? 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
core.drawThumbnail(props.floorId, props.map, options);
ctx.restore();

View File

@ -483,7 +483,6 @@ export class Damage extends RenderItem<EDamageEvent> {
const { ctx } = canvas;
ctx.save();
transformCanvas(canvas, transform);
ctx.lineJoin = 'round';
const render = this.calNeedRender(transform);
const block = this.block;
@ -521,6 +520,8 @@ export class Damage extends RenderItem<EDamageEvent> {
const { ctx: ct } = temp;
ct.translate(-px, -py);
ct.lineJoin = 'round';
ct.lineCap = 'round';
const render = this.renderable.get(v);
@ -532,6 +533,7 @@ export class Damage extends RenderItem<EDamageEvent> {
ct.font = v.font ?? this.font;
ct.strokeStyle = v.stroke ?? this.strokeStyle;
ct.lineWidth = v.strokeWidth ?? this.strokeWidth;
ct.strokeText(v.text, v.x, v.y);
ct.fillText(v.text, v.x, v.y);
});

View File

@ -151,8 +151,23 @@ export type CustomContainerRenderFn = (
transform: Transform
) => void;
export type CustomContainerPropagateOrigin = <T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T]
) => void;
export type CustomContainerPropagateFn = <T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T],
container: ContainerCustom,
origin: CustomContainerPropagateOrigin
) => void;
export class ContainerCustom extends Container {
private renderFn?: CustomContainerRenderFn;
private propagateFn?: CustomContainerPropagateFn;
protected render(
canvas: MotaOffscreenCanvas2D,
@ -165,6 +180,20 @@ export class ContainerCustom extends Container {
}
}
protected propagateEvent<T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T]
): void {
if (this.propagateFn) {
this.propagateFn(type, progress, event, this, () => {
super.propagateEvent(type, progress, event);
});
} else {
super.propagateEvent(type, progress, event);
}
}
/**
*
* @param render
@ -173,6 +202,14 @@ export class ContainerCustom extends Container {
this.renderFn = render;
}
/**
*
* @param propagate
*/
setPropagateFn(propagate: CustomContainerPropagateFn) {
this.propagateFn = propagate;
}
protected handleProps(
key: string,
prevValue: any,
@ -184,6 +221,11 @@ export class ContainerCustom extends Container {
this.setRenderFn(nextValue);
return true;
}
case 'propagate': {
if (!this.assertType(nextValue, 'function', key)) return false;
this.setPropagateFn(nextValue);
return true;
}
}
return super.handleProps(key, prevValue, nextValue);
}

View File

@ -73,11 +73,11 @@ export interface IActionEventBase {
ctrlKey: boolean;
/** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
metaKey: boolean;
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
identifier: number;
}
export interface IActionEvent extends IActionEventBase {
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
identifier: number;
/** 相对于触发元素左上角的横坐标 */
offsetX: number;
/** 相对于触发元素左上角的纵坐标 */

View File

@ -165,14 +165,19 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
}
this.checkMouseEnterLeave(
ev,
event,
this.beforeHovered,
this.hoveredElement
);
});
canvas.addEventListener('mouseleave', ev => {
ev.preventDefault();
const id = this.getMouseIdentifier(
ActionType.Leave,
this.getMouseType(ev)
);
this.hoveredElement.forEach(v => {
v.emit('leave', this.createMouseActionBase(ev, v));
v.emit('leave', this.createMouseActionBase(ev, id, v));
});
this.hoveredElement.clear();
this.beforeHovered.clear();
@ -210,6 +215,7 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
this.captureEvent(ActionType.Move, v);
this.checkTouchEnterLeave(
ev,
v,
this.beforeHovered,
this.hoveredElement
);
@ -308,10 +314,12 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
private createMouseActionBase(
event: MouseEvent,
id: number,
target: RenderItem = this,
mouse: MouseType = this.getMouseType(event)
): IActionEventBase {
return {
identifier: id,
target: target,
touch: false,
type: mouse,
@ -325,9 +333,11 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
private createTouchActionBase(
event: TouchEvent,
id: number,
target: RenderItem
): IActionEventBase {
return {
identifier: id,
target: target,
touch: false,
type: MouseType.Left,
@ -480,36 +490,50 @@ export class MotaRenderer extends Container implements IRenderTreeRoot {
private checkMouseEnterLeave(
event: MouseEvent,
ev: IActionEvent,
before: Set<RenderItem>,
now: Set<RenderItem>
) {
// 先 leave再 enter
before.forEach(v => {
if (!now.has(v)) {
v.emit('leave', this.createMouseActionBase(event, v));
v.emit(
'leave',
this.createMouseActionBase(event, ev.identifier, v)
);
}
});
now.forEach(v => {
if (!before.has(v)) {
v.emit('enter', this.createMouseActionBase(event, v));
v.emit(
'enter',
this.createMouseActionBase(event, ev.identifier, v)
);
}
});
}
private checkTouchEnterLeave(
event: TouchEvent,
ev: IActionEvent,
before: Set<RenderItem>,
now: Set<RenderItem>
) {
// 先 leave再 enter
before.forEach(v => {
if (!now.has(v)) {
v.emit('leave', this.createTouchActionBase(event, v));
v.emit(
'leave',
this.createTouchActionBase(event, ev.identifier, v)
);
}
});
now.forEach(v => {
if (!before.has(v)) {
v.emit('enter', this.createTouchActionBase(event, v));
v.emit(
'enter',
this.createTouchActionBase(event, ev.identifier, v)
);
}
});
}

View File

@ -6,7 +6,8 @@ import {
ElementAnchor,
ElementLocator,
ElementScale,
CustomContainerRenderFn
CustomContainerRenderFn,
CustomContainerPropagateFn
} from '@motajs/render-core';
import {
BezierParams,
@ -94,6 +95,8 @@ export interface ContainerProps extends BaseProps {}
export interface ConatinerCustomProps extends ContainerProps {
/** 自定义容器渲染函数 */
render?: CustomContainerRenderFn;
/** 自定义容器事件传递函数 */
propagate?: CustomContainerPropagateFn;
}
export interface GL2Props extends BaseProps {}

File diff suppressed because it is too large Load Diff