mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-11 15:47:06 +08:00
fix: 滚动条的相关问题 & feat: 自定义容器渲染
This commit is contained in:
parent
6238c8f00a
commit
8bc2a00bd2
@ -1,3 +1,4 @@
|
||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { ActionType, EventProgress, ActionEventMap } from './event';
|
||||
import {
|
||||
@ -14,7 +15,6 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
extends RenderItem<E | EContainerEvent>
|
||||
implements IRenderChildable
|
||||
{
|
||||
children: Set<RenderItem> = new Set();
|
||||
sortedChildren: RenderItem[] = [];
|
||||
|
||||
private needSort: boolean = false;
|
||||
@ -133,7 +133,53 @@ export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.children.forEach(v => {
|
||||
v.destroy();
|
||||
v.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type CustomContainerRenderFn = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => void;
|
||||
|
||||
export class ContainerCustom extends Container {
|
||||
private renderFn?: CustomContainerRenderFn;
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
if (!this.renderFn) {
|
||||
super.render(canvas, transform);
|
||||
} else {
|
||||
this.renderFn(canvas, this.sortedChildren, transform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个自定义容器的渲染函数
|
||||
* @param render 渲染函数
|
||||
*/
|
||||
setRenderFn(render?: CustomContainerRenderFn) {
|
||||
this.renderFn = render;
|
||||
}
|
||||
|
||||
patchProp(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any,
|
||||
namespace?: ElementNamespace,
|
||||
parentComponent?: ComponentInternalInstance | null
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'render': {
|
||||
if (!this.assertType(nextValue, 'function', key)) return;
|
||||
this.setRenderFn(nextValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
}
|
||||
}
|
||||
|
@ -362,6 +362,11 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
this._transform.bind(this);
|
||||
this.cache = this.requireCanvas();
|
||||
this.cache.withGameScale(true);
|
||||
if (!enableCache) {
|
||||
this.cache.withGameScale(false);
|
||||
this.cache.size(1, 1);
|
||||
this.cache.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -437,7 +442,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
size(width: number, height: number): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.cache.size(width, height);
|
||||
if (this.enableCache) {
|
||||
this.cache.size(width, height);
|
||||
}
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
@ -480,13 +487,17 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
|
||||
setHD(hd: boolean): void {
|
||||
this.highResolution = hd;
|
||||
this.cache.setHD(hd);
|
||||
if (this.enableCache) {
|
||||
this.cache.setHD(hd);
|
||||
}
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
setAntiAliasing(anti: boolean): void {
|
||||
this.antiAliasing = anti;
|
||||
this.cache.setAntiAliasing(anti);
|
||||
if (this.enableCache) {
|
||||
this.cache.setAntiAliasing(anti);
|
||||
}
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
@ -540,7 +551,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到可以包围这个元素的最小矩形
|
||||
* 获取到可以包围这个元素的最小矩形,相对于父元素
|
||||
*/
|
||||
getBoundingRect(): DOMRectReadOnly {
|
||||
if (this.type === 'absolute') {
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
BezierProps,
|
||||
CirclesProps,
|
||||
CommentProps,
|
||||
ConatinerCustomProps,
|
||||
ContainerProps,
|
||||
CustomProps,
|
||||
DamageProps,
|
||||
@ -84,6 +85,10 @@ declare module 'vue/jsx-runtime' {
|
||||
export interface IntrinsicElements {
|
||||
sprite: TagDefine<SpriteProps, ESpriteEvent>;
|
||||
container: TagDefine<ContainerProps, EContainerEvent>;
|
||||
'container-custom': TagDefine<
|
||||
ConatinerCustomProps,
|
||||
EContainerEvent
|
||||
>;
|
||||
shader: TagDefine<ShaderProps, EShaderEvent>;
|
||||
text: TagDefine<TextProps, ETextEvent>;
|
||||
image: TagDefine<ImageProps, EImageEvent>;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item';
|
||||
import { ElementNamespace, VNodeProps } from 'vue';
|
||||
import { Container } from '../container';
|
||||
import { Container, ContainerCustom } from '../container';
|
||||
import { MotaRenderer } from '../render';
|
||||
import { Sprite } from '../sprite';
|
||||
import {
|
||||
@ -75,8 +75,13 @@ const standardElement = (
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const { type = 'static', cache = true, fall = false } = props;
|
||||
return new Item(type, cache, fall);
|
||||
const {
|
||||
type = 'static',
|
||||
cache = true,
|
||||
fall = false,
|
||||
nocache = false
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -91,8 +96,13 @@ const standardElementNoCache = (
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const { type = 'static', cache = false, fall = false } = props;
|
||||
return new Item(type, cache, fall);
|
||||
const {
|
||||
type = 'static',
|
||||
cache = false,
|
||||
fall = false,
|
||||
nocache = true
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -124,15 +134,17 @@ const se = (
|
||||
const {
|
||||
type = position,
|
||||
cache = defaultCache,
|
||||
fall = defautFall
|
||||
fall = defautFall,
|
||||
nocache = !defaultCache
|
||||
} = props;
|
||||
return new Item(type, cache, fall);
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Default elements
|
||||
tagMap.register('container', standardElement(Container));
|
||||
tagMap.register('container-custom', standardElement(ContainerCustom));
|
||||
tagMap.register('template', standardElement(Container));
|
||||
tagMap.register('mota-renderer', (_0, _1, props) => {
|
||||
return new MotaRenderer(props?.id);
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
import type { EnemyCollection } from '@/game/enemy/damage';
|
||||
import { ILineProperty } from '../preset/graphics';
|
||||
import { ElementAnchor, ElementLocator } from '../utils';
|
||||
import { CustomContainerRenderFn } from '../container';
|
||||
|
||||
export interface CustomProps {
|
||||
_item: (props: BaseProps) => RenderItem;
|
||||
@ -27,7 +28,10 @@ export interface BaseProps {
|
||||
hidden?: boolean;
|
||||
transform?: Transform;
|
||||
type?: RenderItemPosition;
|
||||
/** 是否启用缓存,用处较少,主要用于一些默认不启用缓存的元素的特殊优化 */
|
||||
cache?: boolean;
|
||||
/** 是否不启用缓存,优先级大于 cache,用处较少,主要用于一些特殊优化 */
|
||||
nocache?: boolean;
|
||||
fall?: boolean;
|
||||
id?: string;
|
||||
alpha?: number;
|
||||
@ -49,6 +53,10 @@ export interface SpriteProps extends BaseProps {
|
||||
|
||||
export interface ContainerProps extends BaseProps {}
|
||||
|
||||
export interface ConatinerCustomProps extends ContainerProps {
|
||||
render?: CustomContainerRenderFn;
|
||||
}
|
||||
|
||||
export interface GL2Props extends BaseProps {}
|
||||
|
||||
export interface ShaderProps extends BaseProps {}
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { Transform } from './transform';
|
||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
import { ActionType, EventProgress, ActionEventMap } from './event';
|
||||
|
||||
export interface ESpriteEvent extends ERenderItemEvent {}
|
||||
|
||||
@ -43,14 +42,6 @@ export class Sprite<
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
protected propagateEvent<T extends ActionType>(
|
||||
type: T,
|
||||
_progress: EventProgress,
|
||||
event: ActionEventMap[T]
|
||||
): void {
|
||||
this.parent?.bubbleEvent(type, event);
|
||||
}
|
||||
|
||||
patchProp(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
reactive,
|
||||
ref,
|
||||
SlotsType,
|
||||
VNode,
|
||||
@ -13,11 +13,10 @@ import {
|
||||
import { SetupComponentOptions } from './types';
|
||||
import {
|
||||
Container,
|
||||
ContainerProps,
|
||||
ElementLocator,
|
||||
RenderItem,
|
||||
Sprite,
|
||||
SpriteProps
|
||||
Transform
|
||||
} from '@/core/render';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { hyper, Transition } from 'mutate-animate';
|
||||
@ -29,9 +28,18 @@ export const enum ScrollDirection {
|
||||
Vertical
|
||||
}
|
||||
|
||||
interface ScrollProps {
|
||||
direction: ScrollDirection;
|
||||
export interface ScrollExpose {
|
||||
/**
|
||||
* 控制滚动条滚动至目标位置
|
||||
* @param y 滚动至的目标位置
|
||||
* @param time 滚动的动画时长,默认为无动画
|
||||
*/
|
||||
scrollTo(y: number, time?: number): void;
|
||||
}
|
||||
|
||||
export interface ScrollProps {
|
||||
loc: ElementLocator;
|
||||
hor?: boolean;
|
||||
noscroll?: boolean;
|
||||
/**
|
||||
* 滚动到最下方(最右方)时的填充大小,如果默认的高度计算方式有误,
|
||||
@ -45,22 +53,47 @@ type ScrollSlots = SlotsType<{
|
||||
}>;
|
||||
|
||||
const scrollProps = {
|
||||
props: ['direction', 'noscroll']
|
||||
props: ['hor', 'noscroll', 'loc', 'padHeight']
|
||||
} satisfies SetupComponentOptions<ScrollProps, {}, string, ScrollSlots>;
|
||||
|
||||
/** 滚动条图示的最短长度 */
|
||||
const SCROLL_MIN_LENGTH = 20;
|
||||
/** 滚动条图示的宽度 */
|
||||
const SCROLL_WIDTH = 10;
|
||||
/** 滚动条的颜色 */
|
||||
const SCROLL_COLOR = '#ddd';
|
||||
|
||||
/**
|
||||
* 滚动条组件,具有虚拟滚动功能,即在画面外的不渲染。参数参考 {@link ScrollProps},暴露接口参考 {@link ScrollExpose}
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* 使用时,建议使用平铺式布局,即包含很多子元素,而不要用一个 container 将所有内容包裹,
|
||||
* 每个子元素的高度(宽度)不建议过大,以更好地通过虚拟滚动优化
|
||||
*
|
||||
* **推荐写法**:
|
||||
* ```tsx
|
||||
* <Scroll>
|
||||
* <item />
|
||||
* <item />
|
||||
* ...其他元素
|
||||
* <item />
|
||||
* <item />
|
||||
* </Scroll>
|
||||
* ```
|
||||
* **不推荐**使用这种写法:
|
||||
* ```tsx
|
||||
* <Scroll>
|
||||
* <container>
|
||||
* <item />
|
||||
* </container>
|
||||
* <Scroll>
|
||||
* ```
|
||||
*/
|
||||
export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
(props, { slots }) => {
|
||||
const scrollProps: SpriteProps = reactive({
|
||||
loc: [0, 0, 0, 0]
|
||||
});
|
||||
const contentProps: ContainerProps = reactive({
|
||||
loc: [0, 0, 0, 0]
|
||||
});
|
||||
(props, { slots, expose }) => {
|
||||
/** 滚动条的定位 */
|
||||
const sp = ref<ElementLocator>([0, 0, 1, 1]);
|
||||
|
||||
const listenedChild: Set<RenderItem> = new Set();
|
||||
const areaMap: Map<RenderItem, [number, number]> = new Map();
|
||||
@ -69,52 +102,63 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
|
||||
const width = computed(() => props.loc[2] ?? 200);
|
||||
const height = computed(() => props.loc[3] ?? 200);
|
||||
const direction = computed(() =>
|
||||
props.hor ? ScrollDirection.Horizontal : ScrollDirection.Vertical
|
||||
);
|
||||
|
||||
let showScroll = 0;
|
||||
let nowScroll = 0;
|
||||
/** 滚动内容的当前位置 */
|
||||
let contentPos = 0;
|
||||
/** 滚动条的当前位置 */
|
||||
let scrollPos = 0;
|
||||
/** 滚动内容的目标位置 */
|
||||
let contentTarget = 0;
|
||||
/** 滚动条的目标位置 */
|
||||
let scrollTarget = 0;
|
||||
/** 滚动内容的长度 */
|
||||
let maxLength = 0;
|
||||
/** 滚动条的长度 */
|
||||
let scrollLength = SCROLL_MIN_LENGTH;
|
||||
|
||||
const transition = new Transition();
|
||||
transition.value.scroll = 0;
|
||||
transition.value.showScroll = 0;
|
||||
transition.mode(hyper('sin', 'out')).absolute();
|
||||
|
||||
//#region 滚动操作
|
||||
|
||||
transition.ticker.add(() => {
|
||||
if (transition.value.scroll !== nowScroll) {
|
||||
showScroll = transition.value.scroll;
|
||||
scroll.value?.update();
|
||||
if (scrollPos !== scrollTarget) {
|
||||
scrollPos = transition.value.scroll;
|
||||
content.value?.update();
|
||||
}
|
||||
if (contentPos !== contentTarget) {
|
||||
contentPos = transition.value.showScroll;
|
||||
checkAllItem();
|
||||
updatePosition();
|
||||
content.value?.update();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.loc,
|
||||
value => {
|
||||
const width = value[2] ?? 200;
|
||||
const height = value[3] ?? 200;
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
props.loc = [0, height - SCROLL_WIDTH, width, SCROLL_WIDTH];
|
||||
} else {
|
||||
props.loc = [width - SCROLL_WIDTH, 0, SCROLL_WIDTH, height];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 滚动到目标值
|
||||
* @param time 动画时长
|
||||
*/
|
||||
const scrollTo = (y: number, time: number = 1) => {
|
||||
const target = clamp(y, 0, maxLength);
|
||||
transition.time(time).transition('scroll', target);
|
||||
nowScroll = y;
|
||||
const scrollTo = (y: number, time: number = 0) => {
|
||||
if (maxLength < height.value) return;
|
||||
const max = maxLength - height.value;
|
||||
const target = clamp(y, 0, max);
|
||||
contentTarget = target;
|
||||
scrollTarget =
|
||||
(height.value - scrollLength) * (contentTarget / max);
|
||||
transition.time(time).transition('scroll', scrollTarget);
|
||||
transition.time(time).transition('showScroll', target);
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算一个元素会在画面上显示的区域
|
||||
*/
|
||||
const getArea = (item: RenderItem, rect: DOMRectReadOnly) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
areaMap.set(item, [rect.left - width.value, rect.right]);
|
||||
} else {
|
||||
areaMap.set(item, [rect.top - height.value, rect.bottom]);
|
||||
@ -131,13 +175,20 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
return;
|
||||
}
|
||||
const [min, max] = area;
|
||||
if (nowScroll > min - 10 && nowScroll < max + 10) {
|
||||
if (contentPos > min - 10 && contentPos < max + 10) {
|
||||
item.show();
|
||||
} else {
|
||||
item.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 对所有元素执行显示检查
|
||||
*/
|
||||
const checkAllItem = () => {
|
||||
content.value?.children.forEach(v => checkItem(v));
|
||||
};
|
||||
|
||||
/**
|
||||
* 当一个元素的矩阵发生变换时执行,检查其显示区域
|
||||
*/
|
||||
@ -147,15 +198,46 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
checkItem(item);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新滚动条位置
|
||||
*/
|
||||
const updatePosition = () => {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
scrollLength = Math.max(
|
||||
SCROLL_MIN_LENGTH,
|
||||
(width.value / maxLength) * width.value
|
||||
);
|
||||
const h = props.noscroll
|
||||
? height.value
|
||||
: height.value - SCROLL_WIDTH;
|
||||
sp.value = [0, h, width.value, SCROLL_WIDTH];
|
||||
} else {
|
||||
scrollLength = clamp(
|
||||
(height.value / maxLength) * height.value,
|
||||
SCROLL_MIN_LENGTH,
|
||||
height.value - 10
|
||||
);
|
||||
const w = props.noscroll
|
||||
? width.value
|
||||
: width.value - SCROLL_WIDTH;
|
||||
sp.value = [w, 0, SCROLL_WIDTH, height.value];
|
||||
}
|
||||
};
|
||||
|
||||
let updating = false;
|
||||
const updateScroll = () => {
|
||||
if (!content.value) return;
|
||||
if (!content.value || updating) return;
|
||||
updating = true;
|
||||
nextTick(() => {
|
||||
updating = false;
|
||||
});
|
||||
let max = 0;
|
||||
listenedChild.forEach(v => v.off('transform', onTransform));
|
||||
listenedChild.clear();
|
||||
areaMap.clear();
|
||||
content.value.children.forEach(v => {
|
||||
const rect = v.getBoundingRect();
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
if (rect.right > max) {
|
||||
max = rect.right;
|
||||
}
|
||||
@ -166,76 +248,95 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
}
|
||||
v.on('transform', onTransform);
|
||||
listenedChild.add(v);
|
||||
checkItem(v);
|
||||
});
|
||||
maxLength = max + (props.padHeight ?? 0);
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
scrollLength = Math.max(
|
||||
SCROLL_MIN_LENGTH,
|
||||
(width.value / max) * width.value
|
||||
);
|
||||
const h = props.noscroll
|
||||
? height.value
|
||||
: height.value - SCROLL_WIDTH;
|
||||
contentProps.loc = [-showScroll, 0, width.value, h];
|
||||
} else {
|
||||
scrollLength = clamp(
|
||||
(height.value / max) * height.value,
|
||||
SCROLL_MIN_LENGTH,
|
||||
height.value - 10
|
||||
);
|
||||
const w = props.noscroll
|
||||
? width.value
|
||||
: width.value - SCROLL_WIDTH;
|
||||
contentProps.loc = [0, -showScroll, w, height.value];
|
||||
}
|
||||
updatePosition();
|
||||
scroll.value?.update();
|
||||
};
|
||||
|
||||
watch(() => props.loc, updateScroll);
|
||||
onUpdated(updateScroll);
|
||||
onMounted(updateScroll);
|
||||
onUnmounted(() => {
|
||||
listenedChild.forEach(v => v.off('transform', onTransform));
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 渲染滚动
|
||||
|
||||
const drawScroll = (canvas: MotaOffscreenCanvas2D) => {
|
||||
if (props.noscroll) return;
|
||||
const ctx = canvas.ctx;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = 6;
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = SCROLL_COLOR;
|
||||
ctx.beginPath();
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
ctx.moveTo(nowScroll + 5, 5);
|
||||
ctx.lineTo(nowScroll + scrollLength + 5, 5);
|
||||
const scroll = transition.value.scroll;
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
ctx.moveTo(scroll + 5, 5);
|
||||
ctx.lineTo(scroll + scrollLength - 5, 5);
|
||||
} else {
|
||||
ctx.moveTo(5, nowScroll + 5);
|
||||
ctx.lineTo(5, nowScroll + scrollLength + 5);
|
||||
ctx.moveTo(5, scroll + 5);
|
||||
ctx.lineTo(5, scroll + scrollLength - 5);
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const renderContent = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.save();
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
ctx.translate(-contentPos, 0);
|
||||
} else {
|
||||
ctx.translate(0, -contentPos);
|
||||
}
|
||||
children.forEach(v => {
|
||||
if (v.hidden) return;
|
||||
v.renderContent(canvas, transform);
|
||||
});
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 事件监听
|
||||
|
||||
const wheelScroll = (delta: number, max: number) => {
|
||||
const sign = Math.sign(delta);
|
||||
const dx = Math.abs(delta);
|
||||
const movement = Math.min(max, dx) * sign;
|
||||
scrollTo(contentTarget + movement, dx > 10 ? 300 : 0);
|
||||
};
|
||||
|
||||
const wheel = (ev: IWheelEvent) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
if (ev.wheelX !== 0) {
|
||||
scrollTo(nowScroll + ev.wheelX, 300);
|
||||
wheelScroll(ev.wheelX, width.value / 5);
|
||||
} else if (ev.wheelY !== 0) {
|
||||
scrollTo(nowScroll + ev.wheelY, 300);
|
||||
wheelScroll(ev.wheelY, width.value / 5);
|
||||
}
|
||||
} else {
|
||||
scrollTo(nowScroll + ev.wheelY, 300);
|
||||
wheelScroll(ev.wheelY, height.value / 5);
|
||||
}
|
||||
};
|
||||
|
||||
const getPos = (ev: IActionEvent) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
return ev.offsetX;
|
||||
} else {
|
||||
return ev.offsetY;
|
||||
}
|
||||
};
|
||||
|
||||
let identifier: number = -1;
|
||||
let lastPos: number = 0;
|
||||
let identifier = -2;
|
||||
let lastPos = 0;
|
||||
|
||||
const down = (ev: IActionEvent) => {
|
||||
identifier = ev.identifier;
|
||||
lastPos = getPos(ev);
|
||||
@ -249,24 +350,32 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
} else {
|
||||
if (ev.buttons & MouseType.Left) {
|
||||
pos = getPos(ev);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const movement = pos - lastPos;
|
||||
scrollTo(nowScroll + movement, 1);
|
||||
|
||||
scrollTo(contentTarget - movement, 0);
|
||||
lastPos = pos;
|
||||
};
|
||||
|
||||
/** 最初滚动条在哪 */
|
||||
let scrollBefore = 0;
|
||||
let scrollIdentifier = -1;
|
||||
/** 本次拖动滚动条的操作标识符 */
|
||||
let scrollIdentifier = -2;
|
||||
/** 点击滚动条时,点击位置在平行于滚动条方向的位置 */
|
||||
let scrollDownPos = 0;
|
||||
/** 是否是点击了滚动条区域中滚动条之外的地方,这样视为类滚轮操作 */
|
||||
let scrollMutate = false;
|
||||
/** 点击滚动条时,点击位置垂直于滚动条方向的位置 */
|
||||
let scrollPin = 0;
|
||||
|
||||
/**
|
||||
* 获取点击滚动条时,垂直于滚动条方向的位置
|
||||
*/
|
||||
const getScrollPin = (ev: IActionEvent) => {
|
||||
if (props.direction === ScrollDirection.Horizontal) {
|
||||
if (direction.value === ScrollDirection.Horizontal) {
|
||||
return ev.absoluteY;
|
||||
} else {
|
||||
return ev.absoluteX;
|
||||
@ -274,13 +383,13 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
};
|
||||
|
||||
const downScroll = (ev: IActionEvent) => {
|
||||
scrollBefore = nowScroll;
|
||||
scrollBefore = contentTarget;
|
||||
scrollIdentifier = ev.identifier;
|
||||
const pos = getPos(ev);
|
||||
// 计算点击在了滚动条的哪个位置
|
||||
const sEnd = nowScroll + scrollLength;
|
||||
if (pos >= nowScroll && pos <= sEnd) {
|
||||
scrollDownPos = pos - nowScroll;
|
||||
const sEnd = contentTarget + scrollLength;
|
||||
if (pos >= contentTarget && pos <= sEnd) {
|
||||
scrollDownPos = pos - contentTarget;
|
||||
scrollMutate = false;
|
||||
scrollPin = getScrollPin(ev);
|
||||
} else {
|
||||
@ -304,22 +413,25 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
threshold = 100;
|
||||
}
|
||||
if (deltaPin > threshold) {
|
||||
scrollTo(scrollBefore, 1);
|
||||
scrollTo(scrollBefore, 0);
|
||||
} else {
|
||||
scrollTo(scrollPos, 1);
|
||||
const pos = (scrollPos / height.value) * maxLength;
|
||||
scrollTo(pos, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const upScroll = (ev: IActionEvent) => {
|
||||
if (!scrollMutate) return;
|
||||
const pos = getPos(ev);
|
||||
if (pos < nowScroll) {
|
||||
if (pos < contentTarget) {
|
||||
scrollTo(pos - 50);
|
||||
} else {
|
||||
scrollTo(pos + 50);
|
||||
}
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
onMounted(() => {
|
||||
scroll.value?.root?.on('move', move);
|
||||
scroll.value?.root?.on('move', moveScroll);
|
||||
@ -328,16 +440,27 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
|
||||
onUnmounted(() => {
|
||||
scroll.value?.root?.off('move', move);
|
||||
scroll.value?.root?.off('move', moveScroll);
|
||||
transition.ticker.destroy();
|
||||
});
|
||||
|
||||
expose<ScrollExpose>({
|
||||
scrollTo
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<container loc={props.loc} onWheel={wheel}>
|
||||
<container {...contentProps} ref={content} onDown={down}>
|
||||
{slots.default()}
|
||||
</container>
|
||||
<container-custom
|
||||
loc={props.loc}
|
||||
ref={content}
|
||||
onDown={down}
|
||||
render={renderContent}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</container-custom>
|
||||
<sprite
|
||||
{...scrollProps}
|
||||
nocache
|
||||
loc={sp.value}
|
||||
ref={scroll}
|
||||
render={drawScroll}
|
||||
onDown={downScroll}
|
||||
|
@ -2,6 +2,7 @@ import { GameUI } from '@/core/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import { SetupComponentOptions } from '../components';
|
||||
import { ElementLocator } from '@/core/render';
|
||||
import { Scroll } from '../components/scroll';
|
||||
|
||||
export interface ILeftHeroStatus {
|
||||
hp: number;
|
||||
@ -168,6 +169,12 @@ export const RightStatusBar = defineComponent<StatusBarProps<IRightHeroStatus>>(
|
||||
return (
|
||||
<container loc={p.loc}>
|
||||
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
|
||||
<Scroll loc={[0, 0, 180, 100]}>
|
||||
<text text="测试1" loc={[0, 0]}></text>
|
||||
<text text="测试2" loc={[0, 50]}></text>
|
||||
<text text="测试3" loc={[0, 100]}></text>
|
||||
<text text="测试4" loc={[0, 200]}></text>
|
||||
</Scroll>
|
||||
</container>
|
||||
);
|
||||
};
|
||||
|
@ -11,7 +11,8 @@ const FSHOST = 'http://127.0.0.1:3000/';
|
||||
|
||||
const custom = [
|
||||
'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom',
|
||||
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin'
|
||||
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin',
|
||||
'container-custom'
|
||||
]
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
|
Loading…
Reference in New Issue
Block a user