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