From 5c8feea512c3c33fd32b9f0875fd86c79e1d3f93 Mon Sep 17 00:00:00 2001 From: Baz Utsahajit Date: Tue, 21 May 2024 01:09:35 +0700 Subject: [PATCH 1/3] Feat: ScrollBox Item Proximity Event --- src/ScrollBox.ts | 63 +++++++++- .../scrollBox/ScrollBoxProximity.stories.ts | 118 ++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/stories/scrollBox/ScrollBoxProximity.stories.ts diff --git a/src/ScrollBox.ts b/src/ScrollBox.ts index ebcaa8ee..8e7defd9 100644 --- a/src/ScrollBox.ts +++ b/src/ScrollBox.ts @@ -1,4 +1,5 @@ import { + Bounds, ColorSource, Container, DestroyOptions, @@ -9,6 +10,7 @@ import { Point, Ticker, } from 'pixi.js'; +import { Signal } from 'typed-signals'; import { List } from './List'; import { Trackpad } from './utils/trackpad/Trackpad'; @@ -25,8 +27,18 @@ export type ScrollBoxOptions = { dragTrashHold?: number; globalScroll?: boolean; shiftScroll?: boolean; + proximityRange?: number; } & Omit; +type ProximityEventData = { + item: Container; + index: number; + inRange: boolean; +}; + +const scrollerBounds = new Bounds(); +const itemBounds = new Bounds(); + /** * Scrollable view, for arranging lists of Pixi container-based elements. * @@ -76,6 +88,12 @@ export class ScrollBox extends Container protected dragStarTouchPoint: Point; protected isOver = false; + protected proximityRange: number; + protected proximityCache: boolean[] = []; + private lastScrollX!: number | null; + private lastScrollY!: number | null; + public onProximityChange = new Signal<(data: ProximityEventData) => void>(); + /** * @param options * @param {number} options.background - background color of the ScrollBox. @@ -128,6 +146,8 @@ export class ScrollBox extends Container this.__width = options.width | this.background.width; this.__height = options.height | this.background.height; + this.proximityRange = options.proximityRange ?? 0; + if (!this.list) { this.list = new List(); @@ -182,6 +202,7 @@ export class ScrollBox extends Container /** Remove all items from a scrollable list. */ removeItems() { + this.proximityCache.length = 0; this.list.removeChildren(); } @@ -207,6 +228,7 @@ export class ScrollBox extends Container child.eventMode = 'static'; this.list.addChild(child); + this.proximityCache.push(false); if (!this.options.disableDynamicRendering) { @@ -226,7 +248,7 @@ export class ScrollBox extends Container removeItem(itemID: number) { this.list.removeItem(itemID); - + this.proximityCache.splice(itemID, 1); this.resize(); } @@ -735,6 +757,45 @@ export class ScrollBox extends Container { this.list[type] = this._trackpad[type]; } + + if (this._trackpad.x !== this.lastScrollX || this._trackpad.y !== this.lastScrollY) + { + /** + * Wait a frame to ensure that the transforms of the scene graph are up-to-date. + * Since we are skipping this step on the 'getBounds' calls for performance's sake, + * this is necessary to ensure that the bounds are accurate. + */ + requestAnimationFrame(() => this.items.forEach((item, index) => this.checkItemProximity(item, index))); + this.lastScrollX = this._trackpad.x; + this.lastScrollY = this._trackpad.y; + } + } + + private checkItemProximity(item: Container, index: number): void + { + /** Get the item bounds, capping the width and height to at least 1 for the purposes of intersection checking. */ + item.getBounds(true, itemBounds); + itemBounds.width = Math.max(itemBounds.width, 1); + itemBounds.height = Math.max(itemBounds.height, 1); + + // Get the scroller bounds, expanding them by the defined max distance. + this.getBounds(true, scrollerBounds); + + scrollerBounds.x -= this.proximityRange; + scrollerBounds.y -= this.proximityRange; + scrollerBounds.width += this.proximityRange * 2; + scrollerBounds.height += this.proximityRange * 2; + + // Check for intersection + const inRange = scrollerBounds.rectangle.intersects(itemBounds.rectangle); + const wasInRange = this.proximityCache[index]; + + // If the item's proximity state has changed, emit the event + if (inRange !== wasInRange) + { + this.proximityCache[index] = inRange; + this.onProximityChange.emit({ item, index, inRange }); + } } /** diff --git a/src/stories/scrollBox/ScrollBoxProximity.stories.ts b/src/stories/scrollBox/ScrollBoxProximity.stories.ts new file mode 100644 index 00000000..61f82286 --- /dev/null +++ b/src/stories/scrollBox/ScrollBoxProximity.stories.ts @@ -0,0 +1,118 @@ +import { Graphics, Text } from 'pixi.js'; +import { PixiStory, StoryFn } from '@pixi/storybook-renderer'; +import { FancyButton } from '../../FancyButton'; +import { ScrollBox } from '../../ScrollBox'; +import { centerElement } from '../../utils/helpers/resize'; +import { defaultTextStyle } from '../../utils/helpers/styles'; +import { argTypes, getDefaultArgs } from '../utils/argTypes'; +import { action } from '@storybook/addon-actions'; + +const args = { + proximityRange: 0, + width: 320, + height: 420, + radius: 20, + elementsMargin: 10, + elementsPadding: 10, + elementsWidth: 300, + elementsHeight: 80, + itemsAmount: 100, + type: [undefined, 'vertical', 'horizontal'], + fadeSpeed: 0.5, +}; + +const items: FancyButton[] = []; +const inRangeCache: boolean[] = []; + +export const ProximityEvent: StoryFn = ({ + width, + height, + radius, + elementsMargin, + elementsPadding, + elementsWidth, + elementsHeight, + itemsAmount, + proximityRange, + type, + fadeSpeed, +}, context) => + new PixiStory({ + context, + init: (view) => + { + const fontColor = '#000000'; + const backgroundColor = '#F5E3A9'; + const disableEasing = false; + const globalScroll = true; + const shiftScroll = type === 'horizontal'; + const onPress = action('Button pressed'); + + items.length = 0; + inRangeCache.length = 0; + + for (let i = 0; i < itemsAmount; i++) + { + const button = new FancyButton({ + defaultView: new Graphics().roundRect(0, 0, elementsWidth, elementsHeight, radius).fill(0xa5e24d), + hoverView: new Graphics().roundRect(0, 0, elementsWidth, elementsHeight, radius).fill(0xfec230), + pressedView: new Graphics().roundRect(0, 0, elementsWidth, elementsHeight, radius).fill(0xfe6048), + text: new Text({ + text: `Item ${i + 1}`, style: { + ...defaultTextStyle, + fill: fontColor + } + }) + }); + + button.anchor.set(0); + button.onPress.connect(() => onPress(i + 1)); + button.alpha = 0; + + items.push(button); + inRangeCache.push(false); + } + + const scrollBox = new ScrollBox({ + background: backgroundColor, + elementsMargin, + width, + height, + radius, + padding: elementsPadding, + disableEasing, + globalScroll, + shiftScroll, + type, + proximityRange, + }); + + scrollBox.addItems(items); + + // Handle on proximity change event. + scrollBox.onProximityChange.connect(({ index, inRange }) => + { + inRangeCache[index] = inRange; + }); + + view.addChild(scrollBox); + }, + resize: (view) => centerElement(view.children[0]), + update: () => + { + items.forEach((item, index) => + { + const inRange = inRangeCache[index]; + + // Fade in/out according to whether the item is within the specified range. + if (inRange && item.alpha < 1) item.alpha += 0.04 * fadeSpeed; + else if (!inRange && item.alpha > 0) item.alpha -= 0.04 * fadeSpeed; + }); + }, + }); + +export default { + title: 'Components/ScrollBox/Proximity Event', + argTypes: argTypes(args), + args: getDefaultArgs(args) +}; From 95c24acdec73e5738358e9ed5f63f53baefce23a Mon Sep 17 00:00:00 2001 From: Baz Utsahajit Date: Tue, 21 May 2024 01:35:11 +0700 Subject: [PATCH 2/3] Change default proximity range on example --- src/stories/scrollBox/ScrollBoxProximity.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/scrollBox/ScrollBoxProximity.stories.ts b/src/stories/scrollBox/ScrollBoxProximity.stories.ts index 61f82286..7c1ff71f 100644 --- a/src/stories/scrollBox/ScrollBoxProximity.stories.ts +++ b/src/stories/scrollBox/ScrollBoxProximity.stories.ts @@ -8,7 +8,7 @@ import { argTypes, getDefaultArgs } from '../utils/argTypes'; import { action } from '@storybook/addon-actions'; const args = { - proximityRange: 0, + proximityRange: 100, width: 320, height: 420, radius: 20, From 525d510cb5e84baf4c691217bff50d9bb272f085 Mon Sep 17 00:00:00 2001 From: Baz Utsahajit Date: Tue, 21 May 2024 15:52:33 +0700 Subject: [PATCH 3/3] [v8] Fix: Incorrect ScrollBox Bidirectional Vertical Flow --- src/List.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/List.ts b/src/List.ts index d321ad60..6fe5eb64 100644 --- a/src/List.ts +++ b/src/List.ts @@ -276,6 +276,7 @@ export class List extends Container */ public arrangeChildren() { + let maxHeight = 0; let x = this.leftPadding; let y = this.topPadding; @@ -311,13 +312,15 @@ export class List extends Container if (child.x + child.width > maxWidth && id > 0) { - y += elementsMargin + child.height; + y += elementsMargin + maxHeight; x = this.leftPadding; child.x = x; child.y = y; + maxHeight = 0; } + maxHeight = Math.max(maxHeight, child.height); x += elementsMargin + child.width; break; }