mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-10-31 04:02:59 +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