UNPKG

outsystems-ui

Version:
468 lines (399 loc) 14.8 kB
// eslint-disable-next-line @typescript-eslint/no-unused-vars namespace OSFramework.OSUI.Behaviors { // SpringAnimation type type SpringAnimationKeyframes = { frames: number; positions: Keyframe[]; }; // Spring Animation properties type SpringAnimationProperties = { friction: number; mass: number; tension: number; }; // Spring Animation configs type SpringAnimationConfigs = { addSpringAnimation: boolean; springAnimationProperties: SpringAnimationProperties; }; /** * Class to hold the drag information * * @class DragParams */ class DragParams { public DragOrientation = GlobalEnum.Orientation.None; public ExpectedDirection = GlobalEnum.Direction.Right; public InvalidDrag = false; public IsMoving = false; public IsOpen = false; public IsReadyToTriggerCallback = false; public LastX = 0; public LastY = 0; public MoveX = 0; public MoveY = 0; public Size = undefined; public SpringAnimation: Animation; public VerticalDrag = false; } /** * Class to manage an element's transform animation on drag * * @export * @class AnimateOnDrag */ export class AnimateOnDrag { // Store the transition information private _dragParams: DragParams; // Store the minimal speed for a swipe to be triggered private readonly _swipeTriggerSpeed = 0.3; // Target element where the transition will be done private _targetElement: HTMLElement; constructor(target: HTMLElement) { this._targetElement = target; this._dragParams = new DragParams(); } // Method to check if current gesture is within pattern boundaries private _checkIsDraggingInsideBounds(currentDrag: number): boolean { const move = this._dragParams.VerticalDrag ? this._dragParams.MoveY : this._dragParams.MoveX; const last = this._dragParams.VerticalDrag ? this._dragParams.LastY : this._dragParams.LastX; const isLeftOrUp = this._dragParams.ExpectedDirection === GlobalEnum.Direction.Left || this._dragParams.ExpectedDirection === GlobalEnum.Direction.Up; const baseThreshold = move + (currentDrag - last); // Check correct threshold for each direction return isLeftOrUp ? baseThreshold > -parseInt(this._dragParams.Size) && move + (currentDrag - last) <= 0 : baseThreshold < parseInt(this._dragParams.Size) && move + (currentDrag - last) >= 0; } // Method to update the last x and y positions private _updateLastPositions(x: number, y: number): void { this._dragParams.LastX = x; this._dragParams.LastY = y; } // Method to apply the transform property on target, depending on the orientation // eslint-disable-next-line @typescript-eslint/naming-convention private _updateUI(): void { if (this._dragParams.IsMoving) { if (this._dragParams.VerticalDrag) { this._targetElement.style.transform = `translateY(${this._dragParams.MoveY}px)`; } else { this._targetElement.style.transform = `translateX(${this._dragParams.MoveX}px)`; } requestAnimationFrame(this._updateUI.bind(this)); } } /** * Get dragParams object * * @readonly * @type {unknown} * @memberof OSFramework.Behaviors.AnimateOnDrag */ public get dragParams(): DragParams { return this._dragParams; } /** * Method to handle the gesture end callback * * @param {number} offsetX * @param {number} offsetY * @param {number} timeTaken * @param {GlobalCallbacks.Generic} callback * @param {SpringAnimationConfigs} [springProperties] * @return {*} {void} * @memberof OSFramework.Behaviors.AnimateOnDrag */ public onDragEnd( offsetX: number, offsetY: number, timeTaken: number, callback: GlobalCallbacks.Generic, springProperties?: SpringAnimationConfigs ): void { this._dragParams.IsMoving = false; // Remove transitions Helper.Dom.Styles.RemoveClass(this._targetElement, Constants.NoTransition); // Check if just clicked or swiped in invalid direction if ((offsetX === 0 && offsetY === 0) || this._dragParams.InvalidDrag) { this._targetElement.style.transform = ''; return; } const checkSwipeSpeed = (this._dragParams.VerticalDrag ? Math.abs(offsetY) : Math.abs(offsetX)) / timeTaken > this._swipeTriggerSpeed; const sizeThreshold = -parseInt(this._dragParams.Size) / 2; const axisToValidate = this._dragParams.VerticalDrag ? this._dragParams.MoveY : this._dragParams.MoveX; // Define a interval for later checks, depending on Sidebar visibility const swipedHalfWidth = axisToValidate < sizeThreshold; // If swipe was fast enough or with sufficient move, procede to toggleSidebar this._dragParams.IsReadyToTriggerCallback = swipedHalfWidth || checkSwipeSpeed; if (this._dragParams.IsReadyToTriggerCallback) { callback(); } else if (springProperties?.addSpringAnimation && this._dragParams.IsOpen) { // Create the animation object this._dragParams.SpringAnimation = SpringAnimation.CreateSpringAnimation( this._targetElement, offsetX, offsetY, this._dragParams.VerticalDrag ? GlobalEnum.Orientation.Vertical : GlobalEnum.Orientation.Horizontal, springProperties.springAnimationProperties ); // Play the animation this._dragParams.SpringAnimation.play(); } this._targetElement.style.transform = ''; } /** * Method to handle the gesture move callback * * @param {number} offsetX * @param {number} offsetY * @param {number} currentX * @param {number} currentY * @param {TouchEvent} event * @return {*} {void} * @memberof OSFramework.Behaviors.AnimateOnDrag */ public onDragMove( offsetX: number, offsetY: number, currentX: number, currentY: number, event: TouchEvent ): void { let _dragDirection; // Check X axis direction if not vertical drag if (!this._dragParams.VerticalDrag) { _dragDirection = offsetX > 0 ? GlobalEnum.Direction.Right : GlobalEnum.Direction.Left; } else { _dragDirection = offsetY < 0 ? GlobalEnum.Direction.Up : GlobalEnum.Direction.Down; } // Set direction as invalid if isOpen and swipe is on opposite direction this._dragParams.InvalidDrag = this._dragParams.IsOpen && _dragDirection !== this._dragParams.ExpectedDirection; // Check if swiped in wrong direction if (this._dragParams.InvalidDrag) { this._updateLastPositions(currentX, currentY); return; } // Check if orientation is set if (this._dragParams.DragOrientation === '') { const isHorizontal = Math.abs(offsetX) >= Math.abs(offsetY); this._dragParams.DragOrientation = isHorizontal ? GlobalEnum.Orientation.Horizontal : GlobalEnum.Orientation.Vertical; requestAnimationFrame(this._updateUI.bind(this)); } // Check if is scrolling if ( this._dragParams.VerticalDrag === false && this._dragParams.DragOrientation === GlobalEnum.Orientation.Vertical ) { this._updateLastPositions(currentX, currentY); return; } // Prevent scrolling the page while doing gesture event.preventDefault(); const IsDraggingInsideBounds = this._checkIsDraggingInsideBounds( this._dragParams.VerticalDrag ? currentY : currentX ); // Checking if dragging inside bounds if (IsDraggingInsideBounds) { if (this._dragParams.VerticalDrag) { // Update y axis offset this._dragParams.MoveY = this._dragParams.MoveY + (currentY - this._dragParams.LastY); } else { // Update x axis offset this._dragParams.MoveX = this._dragParams.MoveX + (currentX - this._dragParams.LastX); } } this._updateLastPositions(currentX, currentY); } /** * Method to handle the gesture start callback * * @param {boolean} verticalDrag * @param {GlobalEnum.Direction} expectedDirection * @param {number} currentX * @param {number} currentY * @param {boolean} isOpen * @param {string} size * @memberof OSFramework.Behaviors.AnimateOnDrag */ public onDragStart( verticalDrag: boolean, expectedDirection: GlobalEnum.Direction, currentX: number, currentY: number, isOpen: boolean, size: string ): void { // Set defaults this._dragParams.DragOrientation = GlobalEnum.Orientation.None; this._dragParams.ExpectedDirection = expectedDirection; this._dragParams.IsMoving = true; this._dragParams.IsOpen = isOpen; this._dragParams.LastX = currentX; this._dragParams.LastY = currentY; this._dragParams.Size = size; this._dragParams.VerticalDrag = verticalDrag; if (this._dragParams.SpringAnimation) { this._dragParams.SpringAnimation.cancel(); } if (this._dragParams.IsOpen) { this._dragParams.MoveX = 0; this._dragParams.MoveY = 0; } else if (this._dragParams.ExpectedDirection === GlobalEnum.Direction.Left) { this._dragParams.MoveX = -parseInt(this._dragParams.Size); this._dragParams.MoveY = -parseInt(this._dragParams.Size); } else { this._dragParams.MoveX = parseInt(this._dragParams.Size); this._dragParams.MoveY = parseInt(this._dragParams.Size); } Helper.Dom.Styles.AddClass(this._targetElement, Constants.NoTransition); } } /** * Class to manage the overlay opacity on a drag transition * * @export * @abstract * @class OverlayTransition */ export abstract class OverlayTransitionOnDrag { /** * Set overlay opacity * * @static * @param {HTMLElement} target * @param {number} currentDragValue * @param {GlobalEnum.Direction} direction * @param {string} size * @memberof OverlayTransition */ public static Set( target: HTMLElement, currentDragValue: number, direction: GlobalEnum.Direction, size: string ): void { const isLeftOrUp = direction === GlobalEnum.Direction.Left || direction === GlobalEnum.Direction.Up; const currentOpacity = parseInt(target.style.getPropertyValue(GlobalEnum.CSSVariables.OverlayOpacity)); const percentageBeforeDif = (Math.abs(currentDragValue) * 100) / parseInt(size); const percentage = isLeftOrUp ? 0 + percentageBeforeDif : 100 - percentageBeforeDif; const newOpacity = Math.floor(percentage) / 100; if (currentOpacity !== newOpacity && newOpacity % 1 !== 0) { Helper.Dom.Styles.SetStyleAttribute(target, GlobalEnum.CSSVariables.OverlayOpacity, newOpacity); } } /** * Unset overlay opacity * * @static * @param {HTMLElement} target * @memberof OverlayTransition */ public static UnSet(target: HTMLElement): void { Helper.Dom.Styles.SetStyleAttribute(target, GlobalEnum.CSSVariables.OverlayOpacity, 0); } } /** * Class to manage the creation of a SpringAnimation * * @export * @abstract * @class SpringAnimation */ export abstract class SpringAnimation { // Method to add a spring effect on dragEnd, when the callback expected is not triggered // based on: https://www.kirillvasiltsov.com/writing/how-to-create-a-spring-animation-with-web-animation-api/ private static _createSpringEffect( dx: number, dy: number, orientation: GlobalEnum.Orientation, springProperties: SpringAnimationProperties ): SpringAnimationKeyframes { if (dx === 0 && dy === 0) return { positions: [], frames: 0 }; // These 3 are used to configure the effect and are common on any spring animation const tension = springProperties.tension; // Tension, as it refers to how tightly-wound the spring is. The tighter the spring, the more energy is released, leading to a snappy, bouncy animation const friction = springProperties.friction; // friction is the force that dampens the motion. Like gravity, as you sprinkle more friction into the universe, the spring becomes less and less bouncy. const mass = springProperties.mass; // Mass refers to the heft of the thing we're moving. A heavier object will move more slowly, but it also has more inertia. We use 1 here for a more simple approach const spring_length = 0; const k = -tension; const d = -friction; const frame_rate = 1 / 60; // Framerate: we want 60 fps hence the framerate here is at 1/60 const displacement_threshold = 3; // Damping is the force that slows down and eventually stops an oscillation by dissipating energy const isVertical = orientation === GlobalEnum.Orientation.Vertical; let velocity = 0; // positions is an array of numbers where each number represents the position of the object in a spring motion at a specific frame const positions = []; let frames = 0; let frames_below_threshold = 0; let largest_displ; // CHange value to be used, depending if is a vertical or horizontal drag let directionDisplacement = isVertical ? dy : dx; for (let step = 0; step <= 1000; step += 1) { const Fspring = k * (directionDisplacement - spring_length); const Fdamping = d * velocity; const accel = (Fspring + Fdamping) / mass; velocity += accel * frame_rate; directionDisplacement += velocity * frame_rate; positions.push({ transform: isVertical ? `translateY(${directionDisplacement}px)` : `translateX(${directionDisplacement}px)`, }); // Save the last largest displacement so that we can compare it with threshold later largest_displ = largest_displ < 0 ? Math.max(largest_displ || -Infinity, Math.sqrt(directionDisplacement ** 2)) : Math.min(largest_displ || Infinity, Math.sqrt(directionDisplacement ** 2)); if (Math.abs(largest_displ) < displacement_threshold) { frames_below_threshold += 1; } else { frames_below_threshold = 0; // Reset the frame counter } if (frames_below_threshold >= 60) { frames = step; break; } } if (frames === 0) { frames = 1000; } return { positions, frames }; } /** * Method to create a Spring Animation * * @static * @param {HTMLElement} target * @param {number} offsetX * @param {number} offsetY * @param {GlobalEnum.Orientation} orientation * @param {SpringAnimationProperties} springProperties * @return {*} {Animation} * @memberof SpringAnimation */ public static CreateSpringAnimation( target: HTMLElement, offsetX: number, offsetY: number, orientation: GlobalEnum.Orientation, springProperties: SpringAnimationProperties ): Animation { // Create the the position for each frame const { positions, frames } = this._createSpringEffect(offsetX, offsetY, orientation, springProperties); // Create the keyframe object const keyframes = new KeyframeEffect(target, positions, { duration: (frames / 60) * 1000, fill: GlobalEnum.KeyframesEffectOptions.FillBoth, easing: GlobalEnum.KeyframesEffectOptions.EasingLinear, iterations: 1, }); // Create the animation object return new Animation(keyframes); } } }