UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

1,056 lines (1,055 loc) 35.5 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug } from "../../../core/debug.js"; import { math } from "../../../core/math/math.js"; import { Vec2 } from "../../../core/math/vec2.js"; import { Vec3 } from "../../../core/math/vec3.js"; import { ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL } from "../../../scene/constants.js"; import { GraphNode } from "../../../scene/graph-node.js"; import { ElementDragHelper } from "../element/element-drag-helper.js"; import { SCROLL_MODE_BOUNCE, SCROLL_MODE_CLAMP, SCROLL_MODE_INFINITE, SCROLLBAR_VISIBILITY_SHOW_ALWAYS, SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED } from "./constants.js"; import { Component } from "../component.js"; const _tempScrollValue = new Vec2(); class ScrollViewComponent extends Component { /** * Create a new ScrollViewComponent. * * @param {ScrollViewComponentSystem} system - The ComponentSystem that created this Component. * @param {Entity} entity - The Entity that this Component is attached to. */ constructor(system, entity) { super(system, entity); /** * @type {boolean|undefined} * @private */ __publicField(this, "_horizontal"); /** * @type {boolean|undefined} * @private */ __publicField(this, "_vertical"); /** * @type {number|undefined} * @private */ __publicField(this, "_scrollMode"); /** * @type {number|undefined} * @private */ __publicField(this, "_bounceAmount"); /** * @type {number|undefined} * @private */ __publicField(this, "_friction"); /** @private */ __publicField(this, "_dragThreshold", 10); /** @private */ __publicField(this, "_useMouseWheel", true); /** @private */ __publicField(this, "_mouseWheelSensitivity", new Vec2(1, 1)); /** @private */ __publicField(this, "_horizontalScrollbarVisibility", 0); /** @private */ __publicField(this, "_verticalScrollbarVisibility", 0); /** * @type {Entity|null} * @private */ __publicField(this, "_viewportEntity", null); /** * @type {Entity|null} * @private */ __publicField(this, "_contentEntity", null); /** * @type {Entity|null} * @private */ __publicField(this, "_horizontalScrollbarEntity", null); /** * @type {Entity|null} * @private */ __publicField(this, "_verticalScrollbarEntity", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtElementAdd", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtElementRemove", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtViewportEntityElementAdd", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtViewportElementRemove", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtViewportResize", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtContentEntityElementAdd", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtContentElementRemove", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtContentResize", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtHorizontalScrollbarAdd", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtHorizontalScrollbarRemove", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtHorizontalScrollbarValue", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtVerticalScrollbarAdd", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtVerticalScrollbarRemove", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtVerticalScrollbarValue", null); this._scrollbarUpdateFlags = {}; this._scrollbarEntities = {}; this._prevContentSizes = {}; this._prevContentSizes[ORIENTATION_HORIZONTAL] = null; this._prevContentSizes[ORIENTATION_VERTICAL] = null; this._scroll = new Vec2(); this._velocity = new Vec3(); this._dragStartPosition = new Vec3(); this._disabledContentInput = false; this._disabledContentInputEntities = []; this._evtElementAdd = this.entity.on("element:add", this._onElementComponentAdd, this); this._toggleElementListeners("on"); } /** * Sets whether horizontal scrolling is enabled. * * @type {boolean} */ set horizontal(arg) { if (this._horizontal === arg) { return; } this._horizontal = arg; this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); } /** * Gets whether horizontal scrolling is enabled. * * @type {boolean} */ get horizontal() { return this._horizontal; } /** * Sets whether vertical scrolling is enabled. * * @type {boolean} */ set vertical(arg) { if (this._vertical === arg) { return; } this._vertical = arg; this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); } /** * Gets whether vertical scrolling is enabled. * * @type {boolean} */ get vertical() { return this._vertical; } /** * Sets the scroll mode of the scroll viewer. Specifies how the scroll view should behave when * the user scrolls past the end of the content. Modes are defined as follows: * * - {@link SCROLL_MODE_CLAMP}: Content does not scroll any further than its bounds. * - {@link SCROLL_MODE_BOUNCE}: Content scrolls past its bounds and then gently bounces back. * - {@link SCROLL_MODE_INFINITE}: Content can scroll forever. * * @type {number} */ set scrollMode(arg) { this._scrollMode = arg; } /** * Gets the scroll mode of the scroll viewer. * * @type {number} */ get scrollMode() { return this._scrollMode; } /** * Sets how far the content should move before bouncing back. * * @type {number} */ set bounceAmount(arg) { this._bounceAmount = arg; } /** * Gets how far the content should move before bouncing back. * * @type {number} */ get bounceAmount() { return this._bounceAmount; } /** * Sets how freely the content should move if thrown, i.e. By flicking on a phone or by * flinging the scroll wheel on a mouse. A value of 1 means that content will stop immediately; * 0 means that content will continue moving forever (or until the bounds of the content are * reached, depending on the scrollMode). * * @type {number} */ set friction(arg) { this._friction = arg; } /** * Gets how freely the content should move if thrown. * * @type {number} */ get friction() { return this._friction; } set dragThreshold(arg) { this._dragThreshold = arg; } get dragThreshold() { return this._dragThreshold; } /** * Sets whether to use mouse wheel for scrolling (horizontally and vertically). * * @type {boolean} */ set useMouseWheel(arg) { this._useMouseWheel = arg; } /** * Gets whether to use mouse wheel for scrolling (horizontally and vertically). * * @type {boolean} */ get useMouseWheel() { return this._useMouseWheel; } /** * Sets the mouse wheel horizontal and vertical sensitivity. Only used if useMouseWheel is set. * Setting a direction to 0 will disable mouse wheel scrolling in that direction. 1 is a * default sensitivity that is considered to feel good. The values can be set higher or lower * than 1 to tune the sensitivity. Defaults to [1, 1]. * * @type {Vec2} */ set mouseWheelSensitivity(arg) { if (!arg) { this._mouseWheelSensitivity = arg; } else if (arg instanceof Vec2) { this._mouseWheelSensitivity = arg.clone(); } else { this._mouseWheelSensitivity = new Vec2(arg[0], arg[1]); } } /** * Gets the mouse wheel horizontal and vertical sensitivity. * * @type {Vec2} */ get mouseWheelSensitivity() { return this._mouseWheelSensitivity; } /** * Sets whether the horizontal scrollbar should be visible all the time, or only visible when * the content exceeds the size of the viewport. * * @type {number} */ set horizontalScrollbarVisibility(arg) { this._horizontalScrollbarVisibility = arg; } /** * Gets whether the horizontal scrollbar should be visible all the time, or only visible when * the content exceeds the size of the viewport. * * @type {number} */ get horizontalScrollbarVisibility() { return this._horizontalScrollbarVisibility; } /** * Sets whether the vertical scrollbar should be visible all the time, or only visible when the * content exceeds the size of the viewport. * * @type {number} */ set verticalScrollbarVisibility(arg) { this._verticalScrollbarVisibility = arg; } /** * Gets whether the vertical scrollbar should be visible all the time, or only visible when the * content exceeds the size of the viewport. * * @type {number} */ get verticalScrollbarVisibility() { return this._verticalScrollbarVisibility; } /** * Sets the entity to be used as the masked viewport area, within which the content will scroll. * This entity must have an ElementGroup component. * * @type {Entity|string|null} */ set viewportEntity(arg) { if (this._viewportEntity === arg) { return; } const isString = typeof arg === "string"; if (this._viewportEntity && isString && this._viewportEntity.guid === arg) { return; } if (this._viewportEntity) { this._viewportEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._viewportEntity = arg; } else if (isString) { this._viewportEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._viewportEntity = null; } if (this._viewportEntity) { this._viewportEntitySubscribe(); } } /** * Gets the entity to be used as the masked viewport area, within which the content will scroll. * * @type {Entity|null} */ get viewportEntity() { return this._viewportEntity; } /** * Sets the entity which contains the scrolling content itself. This entity must have an * {@link ElementComponent}. * * @type {Entity|string|null} */ set contentEntity(arg) { if (this._contentEntity === arg) { return; } const isString = typeof arg === "string"; if (this._contentEntity && isString && this._contentEntity.guid === arg) { return; } if (this._contentEntity) { this._contentEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._contentEntity = arg; } else if (isString) { this._contentEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._contentEntity = null; } if (this._contentEntity) { this._contentEntitySubscribe(); } } /** * Gets the entity which contains the scrolling content itself. * * @type {Entity|null} */ get contentEntity() { return this._contentEntity; } /** * Sets the entity to be used as the horizontal scrollbar. This entity must have a * {@link ScrollbarComponent}. * * @type {Entity|string|null} */ set horizontalScrollbarEntity(arg) { if (this._horizontalScrollbarEntity === arg) { return; } const isString = typeof arg === "string"; if (this._horizontalScrollbarEntity && isString && this._horizontalScrollbarEntity.guid === arg) { return; } if (this._horizontalScrollbarEntity) { this._horizontalScrollbarEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._horizontalScrollbarEntity = arg; } else if (isString) { this._horizontalScrollbarEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._horizontalScrollbarEntity = null; } this._scrollbarEntities[ORIENTATION_HORIZONTAL] = this._horizontalScrollbarEntity; if (this._horizontalScrollbarEntity) { this._horizontalScrollbarEntitySubscribe(); } } /** * Gets the entity to be used as the horizontal scrollbar. * * @type {Entity|null} */ get horizontalScrollbarEntity() { return this._horizontalScrollbarEntity; } /** * Sets the entity to be used as the vertical scrollbar. This entity must have a * {@link ScrollbarComponent}. * * @type {Entity|string|null} */ set verticalScrollbarEntity(arg) { if (this._verticalScrollbarEntity === arg) { return; } const isString = typeof arg === "string"; if (this._verticalScrollbarEntity && isString && this._verticalScrollbarEntity.guid === arg) { return; } if (this._verticalScrollbarEntity) { this._verticalScrollbarEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._verticalScrollbarEntity = arg; } else if (isString) { this._verticalScrollbarEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._verticalScrollbarEntity = null; } this._scrollbarEntities[ORIENTATION_VERTICAL] = this._verticalScrollbarEntity; if (this._verticalScrollbarEntity) { this._verticalScrollbarEntitySubscribe(); } } /** * Gets the entity to be used as the vertical scrollbar. * * @type {Entity|null} */ get verticalScrollbarEntity() { return this._verticalScrollbarEntity; } /** * Sets the scroll value. * * @type {Vec2} */ set scroll(value) { this._onSetScroll(value.x, value.y); } /** * Gets the scroll value. * * @type {Vec2} */ get scroll() { return this._scroll; } /** * @param {string} onOrOff - 'on' or 'off'. * @private */ _toggleElementListeners(onOrOff) { if (this.entity.element) { if (onOrOff === "on" && this._hasElementListeners) { return; } this.entity.element[onOrOff]("resize", this._syncAll, this); this.entity.element[onOrOff]("mousewheel", this._onMouseWheel, this); this._hasElementListeners = onOrOff === "on"; } } _onElementComponentAdd(entity) { this._evtElementRemove = this.entity.element.once("beforeremove", this._onElementComponentRemove, this); this._toggleElementListeners("on"); } _onElementComponentRemove(entity) { this._evtElementRemove?.off(); this._evtElementRemove = null; this._toggleElementListeners("off"); } _viewportEntitySubscribe() { this._evtViewportEntityElementAdd = this._viewportEntity.on("element:add", this._onViewportElementGain, this); if (this._viewportEntity.element) { this._onViewportElementGain(); } } _viewportEntityUnsubscribe() { this._evtViewportEntityElementAdd?.off(); this._evtViewportEntityElementAdd = null; if (this._viewportEntity?.element) { this._onViewportElementLose(); } } _viewportEntityElementSubscribe() { const element = this._viewportEntity.element; this._evtViewportElementRemove = element.once("beforeremove", this._onViewportElementLose, this); this._evtViewportResize = element.on("resize", this._syncAll, this); } _viewportEntityElementUnsubscribe() { this._evtViewportElementRemove?.off(); this._evtViewportElementRemove = null; this._evtViewportResize?.off(); this._evtViewportResize = null; } _onViewportElementGain() { this._viewportEntityElementSubscribe(); this._syncAll(); } _onViewportElementLose() { this._viewportEntityElementUnsubscribe(); } _contentEntitySubscribe() { this._evtContentEntityElementAdd = this._contentEntity.on("element:add", this._onContentElementGain, this); if (this._contentEntity.element) { this._onContentElementGain(); } } _contentEntityUnsubscribe() { this._evtContentEntityElementAdd?.off(); this._evtContentEntityElementAdd = null; if (this._contentEntity?.element) { this._onContentElementLose(); } } _contentEntityElementSubscribe() { const element = this._contentEntity.element; this._evtContentElementRemove = element.once("beforeremove", this._onContentElementLose, this); this._evtContentResize = element.on("resize", this._syncAll, this); } _contentEntityElementUnsubscribe() { this._evtContentElementRemove?.off(); this._evtContentElementRemove = null; this._evtContentResize?.off(); this._evtContentResize = null; } _onContentElementGain() { this._contentEntityElementSubscribe(); this._destroyDragHelper(); this._contentDragHelper = new ElementDragHelper(this._contentEntity.element); this._contentDragHelper.on("drag:start", this._onContentDragStart, this); this._contentDragHelper.on("drag:end", this._onContentDragEnd, this); this._contentDragHelper.on("drag:move", this._onContentDragMove, this); this._prevContentSizes[ORIENTATION_HORIZONTAL] = null; this._prevContentSizes[ORIENTATION_VERTICAL] = null; this._syncAll(); } _onContentElementLose() { this._contentEntityElementUnsubscribe(); this._destroyDragHelper(); } _onContentDragStart() { if (this._contentEntity && this.enabled && this.entity.enabled) { this._dragStartPosition.copy(this._contentEntity.getLocalPosition()); } } _onContentDragEnd() { this._prevContentDragPosition = null; this._enableContentInput(); } _onContentDragMove(position) { if (this._contentEntity && this.enabled && this.entity.enabled) { this._wasDragged = true; this._setScrollFromContentPosition(position); this._setVelocityFromContentPositionDelta(position); if (!this._disabledContentInput) { const dx = position.x - this._dragStartPosition.x; const dy = position.y - this._dragStartPosition.y; if (Math.abs(dx) > this.dragThreshold || Math.abs(dy) > this.dragThreshold) { this._disableContentInput(); } } } } _horizontalScrollbarEntitySubscribe() { this._evtHorizontalScrollbarAdd = this._horizontalScrollbarEntity.on("scrollbar:add", this._onHorizontalScrollbarGain, this); if (this._horizontalScrollbarEntity.scrollbar) { this._onHorizontalScrollbarGain(); } } _verticalScrollbarEntitySubscribe() { this._evtVerticalScrollbarAdd = this._verticalScrollbarEntity.on("scrollbar:add", this._onVerticalScrollbarGain, this); if (this._verticalScrollbarEntity.scrollbar) { this._onVerticalScrollbarGain(); } } _horizontalScrollbarEntityUnsubscribe() { this._evtHorizontalScrollbarAdd?.off(); this._evtHorizontalScrollbarAdd = null; if (this._horizontalScrollbarEntity.scrollbar) { this._onHorizontalScrollbarLose(); } } _verticalScrollbarEntityUnsubscribe() { this._evtVerticalScrollbarAdd?.off(); this._evtVerticalScrollbarAdd = null; if (this._verticalScrollbarEntity.scrollbar) { this._onVerticalScrollbarLose(); } } _onSetHorizontalScrollbarValue(scrollValueX) { if (!this._scrollbarUpdateFlags[ORIENTATION_HORIZONTAL] && this.enabled && this.entity.enabled) { this._onSetScroll(scrollValueX, null); } } _onSetVerticalScrollbarValue(scrollValueY) { if (!this._scrollbarUpdateFlags[ORIENTATION_VERTICAL] && this.enabled && this.entity.enabled) { this._onSetScroll(null, scrollValueY); } } _onHorizontalScrollbarGain() { const scrollbar = this._horizontalScrollbarEntity?.scrollbar; this._evtHorizontalScrollbarRemove = scrollbar.on("beforeremove", this._onHorizontalScrollbarLose, this); this._evtHorizontalScrollbarValue = scrollbar.on("set:value", this._onSetHorizontalScrollbarValue, this); this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); this._syncScrollbarPosition(ORIENTATION_HORIZONTAL); } _onVerticalScrollbarGain() { const scrollbar = this._verticalScrollbarEntity?.scrollbar; this._evtVerticalScrollbarRemove = scrollbar.on("beforeremove", this._onVerticalScrollbarLose, this); this._evtVerticalScrollbarValue = scrollbar.on("set:value", this._onSetVerticalScrollbarValue, this); this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); this._syncScrollbarPosition(ORIENTATION_VERTICAL); } _onHorizontalScrollbarLose() { this._evtHorizontalScrollbarRemove?.off(); this._evtHorizontalScrollbarRemove = null; this._evtHorizontalScrollbarValue?.off(); this._evtHorizontalScrollbarValue = null; } _onVerticalScrollbarLose() { this._evtVerticalScrollbarRemove?.off(); this._evtVerticalScrollbarRemove = null; this._evtVerticalScrollbarValue?.off(); this._evtVerticalScrollbarValue = null; } _onSetScroll(x, y, resetVelocity) { if (resetVelocity !== false) { this._velocity.set(0, 0, 0); } const xChanged = this._updateAxis(x, "x", ORIENTATION_HORIZONTAL); const yChanged = this._updateAxis(y, "y", ORIENTATION_VERTICAL); if (xChanged || yChanged) { this.fire("set:scroll", this._scroll); } } _updateAxis(scrollValue, axis, orientation) { const hasChanged = scrollValue !== null && Math.abs(scrollValue - this._scroll[axis]) > 1e-5; if (hasChanged || this._isDragging() || scrollValue === 0) { this._scroll[axis] = this._determineNewScrollValue(scrollValue, axis, orientation); this._syncContentPosition(orientation); this._syncScrollbarPosition(orientation); } return hasChanged; } _determineNewScrollValue(scrollValue, axis, orientation) { if (!this._getScrollingEnabled(orientation)) { return this._scroll[axis]; } switch (this.scrollMode) { case SCROLL_MODE_CLAMP: return math.clamp(scrollValue, 0, this._getMaxScrollValue(orientation)); case SCROLL_MODE_BOUNCE: this._setVelocityFromOvershoot(scrollValue, axis, orientation); return scrollValue; case SCROLL_MODE_INFINITE: return scrollValue; default: console.warn(`Unhandled scroll mode:${this.scrollMode}`); return scrollValue; } } _syncAll() { this._syncContentPosition(ORIENTATION_HORIZONTAL); this._syncContentPosition(ORIENTATION_VERTICAL); this._syncScrollbarPosition(ORIENTATION_HORIZONTAL); this._syncScrollbarPosition(ORIENTATION_VERTICAL); this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); } _syncContentPosition(orientation) { if (!this._contentEntity) { return; } const axis = this._getAxis(orientation); const sign = this._getSign(orientation); const prevContentSize = this._prevContentSizes[orientation]; const currContentSize = this._getContentSize(orientation); if (prevContentSize !== null && Math.abs(prevContentSize - currContentSize) > 1e-4) { const prevMaxOffset = this._getMaxOffset(orientation, prevContentSize); const currMaxOffset = this._getMaxOffset(orientation, currContentSize); if (currMaxOffset === 0) { this._scroll[axis] = 1; } else { this._scroll[axis] = math.clamp(this._scroll[axis] * prevMaxOffset / currMaxOffset, 0, 1); } } const offset = this._scroll[axis] * this._getMaxOffset(orientation); const contentPosition = this._contentEntity.getLocalPosition(); contentPosition[axis] = offset * sign; this._contentEntity.setLocalPosition(contentPosition); this._prevContentSizes[orientation] = currContentSize; } _syncScrollbarPosition(orientation) { const scrollbarEntity = this._scrollbarEntities[orientation]; if (!scrollbarEntity?.scrollbar) { return; } const axis = this._getAxis(orientation); this._scrollbarUpdateFlags[orientation] = true; scrollbarEntity.scrollbar.value = this._scroll[axis]; scrollbarEntity.scrollbar.handleSize = this._getScrollbarHandleSize(axis, orientation); this._scrollbarUpdateFlags[orientation] = false; } // Toggles the scrollbar entities themselves to be enabled/disabled based // on whether the user has enabled horizontal/vertical scrolling on the // scroll view. _syncScrollbarEnabledState(orientation) { const entity = this._scrollbarEntities[orientation]; if (!entity) { return; } const isScrollingEnabled = this._getScrollingEnabled(orientation); const requestedVisibility = this._getScrollbarVisibility(orientation); switch (requestedVisibility) { case SCROLLBAR_VISIBILITY_SHOW_ALWAYS: entity.enabled = isScrollingEnabled; return; case SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED: entity.enabled = isScrollingEnabled && this._contentIsLargerThanViewport(orientation); return; default: console.warn(`Unhandled scrollbar visibility:${requestedVisibility}`); entity.enabled = isScrollingEnabled; } } _contentIsLargerThanViewport(orientation) { return this._getContentSize(orientation) > this._getViewportSize(orientation); } _contentPositionToScrollValue(contentPosition) { const maxOffsetH = this._getMaxOffset(ORIENTATION_HORIZONTAL); const maxOffsetV = this._getMaxOffset(ORIENTATION_VERTICAL); if (maxOffsetH === 0) { _tempScrollValue.x = 0; } else { _tempScrollValue.x = contentPosition.x / maxOffsetH; } if (maxOffsetV === 0) { _tempScrollValue.y = 0; } else { _tempScrollValue.y = contentPosition.y / -maxOffsetV; } return _tempScrollValue; } _getMaxOffset(orientation, contentSize) { contentSize = contentSize === void 0 ? this._getContentSize(orientation) : contentSize; const viewportSize = this._getViewportSize(orientation); if (contentSize < viewportSize) { return -this._getViewportSize(orientation); } return viewportSize - contentSize; } _getMaxScrollValue(orientation) { return this._contentIsLargerThanViewport(orientation) ? 1 : 0; } _getScrollbarHandleSize(axis, orientation) { const viewportSize = this._getViewportSize(orientation); const contentSize = this._getContentSize(orientation); if (Math.abs(contentSize) < 1e-3) { return 1; } const handleSize = Math.min(viewportSize / contentSize, 1); const overshoot = this._toOvershoot(this._scroll[axis], orientation); if (overshoot === 0) { return handleSize; } return handleSize / (1 + Math.abs(overshoot)); } _getViewportSize(orientation) { return this._getSize(orientation, this._viewportEntity); } _getContentSize(orientation) { return this._getSize(orientation, this._contentEntity); } _getSize(orientation, entity) { if (entity?.element) { return entity.element[this._getCalculatedDimension(orientation)]; } return 0; } _getScrollingEnabled(orientation) { if (orientation === ORIENTATION_HORIZONTAL) { return this.horizontal; } else if (orientation === ORIENTATION_VERTICAL) { return this.vertical; } Debug.warn(`Unrecognized orientation: ${orientation}`); return void 0; } _getScrollbarVisibility(orientation) { if (orientation === ORIENTATION_HORIZONTAL) { return this.horizontalScrollbarVisibility; } else if (orientation === ORIENTATION_VERTICAL) { return this.verticalScrollbarVisibility; } Debug.warn(`Unrecognized orientation: ${orientation}`); return void 0; } _getSign(orientation) { return orientation === ORIENTATION_HORIZONTAL ? 1 : -1; } _getAxis(orientation) { return orientation === ORIENTATION_HORIZONTAL ? "x" : "y"; } _getCalculatedDimension(orientation) { return orientation === ORIENTATION_HORIZONTAL ? "calculatedWidth" : "calculatedHeight"; } _destroyDragHelper() { if (this._contentDragHelper) { this._contentDragHelper.destroy(); } } onUpdate() { if (this._contentEntity) { this._updateVelocity(); this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); } } _updateVelocity() { if (!this._isDragging()) { if (this.scrollMode === SCROLL_MODE_BOUNCE) { if (this._hasOvershoot("x", ORIENTATION_HORIZONTAL)) { this._setVelocityFromOvershoot(this.scroll.x, "x", ORIENTATION_HORIZONTAL); } if (this._hasOvershoot("y", ORIENTATION_VERTICAL)) { this._setVelocityFromOvershoot(this.scroll.y, "y", ORIENTATION_VERTICAL); } } if (Math.abs(this._velocity.x) > 1e-4 || Math.abs(this._velocity.y) > 1e-4) { const position = this._contentEntity.getLocalPosition(); position.x += this._velocity.x; position.y += this._velocity.y; this._contentEntity.setLocalPosition(position); this._setScrollFromContentPosition(position); } this._velocity.x *= 1 - this.friction; this._velocity.y *= 1 - this.friction; } } _hasOvershoot(axis, orientation) { return Math.abs(this._toOvershoot(this.scroll[axis], orientation)) > 1e-3; } _toOvershoot(scrollValue, orientation) { const maxScrollValue = this._getMaxScrollValue(orientation); if (scrollValue < 0) { return scrollValue; } else if (scrollValue > maxScrollValue) { return scrollValue - maxScrollValue; } return 0; } _setVelocityFromOvershoot(scrollValue, axis, orientation) { const overshootValue = this._toOvershoot(scrollValue, orientation); const overshootPixels = overshootValue * this._getMaxOffset(orientation) * this._getSign(orientation); if (Math.abs(overshootPixels) > 0) { this._velocity[axis] = -overshootPixels / (this.bounceAmount * 50 + 1); } } _setVelocityFromContentPositionDelta(position) { if (this._prevContentDragPosition) { this._velocity.sub2(position, this._prevContentDragPosition); this._prevContentDragPosition.copy(position); } else { this._velocity.set(0, 0, 0); this._prevContentDragPosition = position.clone(); } } _setScrollFromContentPosition(position) { let scrollValue = this._contentPositionToScrollValue(position); if (this._isDragging()) { scrollValue = this._applyScrollValueTension(scrollValue); } this._onSetScroll(scrollValue.x, scrollValue.y, false); } // Create nice tension effect when dragging past the extents of the viewport _applyScrollValueTension(scrollValue) { const factor = 1; let max = this._getMaxScrollValue(ORIENTATION_HORIZONTAL); let overshoot = this._toOvershoot(scrollValue.x, ORIENTATION_HORIZONTAL); if (overshoot > 0) { scrollValue.x = max + factor * Math.log10(1 + overshoot); } else if (overshoot < 0) { scrollValue.x = -factor * Math.log10(1 - overshoot); } max = this._getMaxScrollValue(ORIENTATION_VERTICAL); overshoot = this._toOvershoot(scrollValue.y, ORIENTATION_VERTICAL); if (overshoot > 0) { scrollValue.y = max + factor * Math.log10(1 + overshoot); } else if (overshoot < 0) { scrollValue.y = -factor * Math.log10(1 - overshoot); } return scrollValue; } _isDragging() { return this._contentDragHelper && this._contentDragHelper.isDragging; } _setScrollbarComponentsEnabled(enabled) { if (this._horizontalScrollbarEntity?.scrollbar) { this._horizontalScrollbarEntity.scrollbar.enabled = enabled; } if (this._verticalScrollbarEntity?.scrollbar) { this._verticalScrollbarEntity.scrollbar.enabled = enabled; } } _setContentDraggingEnabled(enabled) { if (this._contentDragHelper) { this._contentDragHelper.enabled = enabled; } } _onMouseWheel(event) { if (!this.useMouseWheel || !this._contentEntity?.element) { return; } const wheelEvent = event.event; const normalizedDeltaX = wheelEvent.deltaX / this._contentEntity.element.calculatedWidth * this.mouseWheelSensitivity.x; const normalizedDeltaY = wheelEvent.deltaY / this._contentEntity.element.calculatedHeight * this.mouseWheelSensitivity.y; const scrollX = math.clamp(this._scroll.x + normalizedDeltaX, 0, this._getMaxScrollValue(ORIENTATION_HORIZONTAL)); const scrollY = math.clamp(this._scroll.y + normalizedDeltaY, 0, this._getMaxScrollValue(ORIENTATION_VERTICAL)); this.scroll = new Vec2(scrollX, scrollY); } // re-enable useInput flag on any descendant that was disabled _enableContentInput() { while (this._disabledContentInputEntities.length) { const e = this._disabledContentInputEntities.pop(); if (e.element) { e.element.useInput = true; } } this._disabledContentInput = false; } // disable useInput flag on all descendants of this contentEntity _disableContentInput() { const _disableInput = (e) => { if (e.element && e.element.useInput) { this._disabledContentInputEntities.push(e); e.element.useInput = false; } const children = e.children; for (let i = 0, l = children.length; i < l; i++) { _disableInput(children[i]); } }; if (this._contentEntity) { const children = this._contentEntity.children; for (let i = 0, l = children.length; i < l; i++) { _disableInput(children[i]); } } this._disabledContentInput = true; } onEnable() { this._setScrollbarComponentsEnabled(true); this._setContentDraggingEnabled(true); this._syncAll(); } onDisable() { this._setScrollbarComponentsEnabled(false); this._setContentDraggingEnabled(false); } onRemove() { this._evtElementAdd?.off(); this._evtElementAdd = null; this._evtElementRemove?.off(); this._evtElementRemove = null; this.viewportEntity = null; this.contentEntity = null; this.horizontalScrollbarEntity = null; this.verticalScrollbarEntity = null; this._toggleElementListeners("off"); this._destroyDragHelper(); } resolveDuplicatedEntityReferenceProperties(oldScrollView, duplicatedIdsMap) { if (oldScrollView.viewportEntity) { this.viewportEntity = duplicatedIdsMap[oldScrollView.viewportEntity.guid]; } if (oldScrollView.contentEntity) { this.contentEntity = duplicatedIdsMap[oldScrollView.contentEntity.guid]; } if (oldScrollView.horizontalScrollbarEntity) { this.horizontalScrollbarEntity = duplicatedIdsMap[oldScrollView.horizontalScrollbarEntity.guid]; } if (oldScrollView.verticalScrollbarEntity) { this.verticalScrollbarEntity = duplicatedIdsMap[oldScrollView.verticalScrollbarEntity.guid]; } } } /** * Fired whenever the scroll position changes. The handler is passed a {@link Vec2} containing * the horizontal and vertical scroll values in the range 0..1. * * @event * @example * entity.scrollview.on('set:scroll', (scroll) => { * console.log(`Horizontal scroll position: ${scroll.x}`); * console.log(`Vertical scroll position: ${scroll.y}`); * }); */ __publicField(ScrollViewComponent, "EVENT_SETSCROLL", "set:scroll"); export { ScrollViewComponent };