morethanwords
4 years ago
19 changed files with 577 additions and 305 deletions
@ -0,0 +1,52 @@ |
|||||||
|
// * Jolly Cobra's animation.ts
|
||||||
|
|
||||||
|
import { fastRaf } from './schedulers'; |
||||||
|
import { CancellablePromise, deferredPromise } from './cancellablePromise'; |
||||||
|
|
||||||
|
interface AnimationInstance { |
||||||
|
isCancelled: boolean; |
||||||
|
deferred: CancellablePromise<void> |
||||||
|
} |
||||||
|
|
||||||
|
type AnimationInstanceKey = any; |
||||||
|
const instances: Map<AnimationInstanceKey, AnimationInstance> = new Map(); |
||||||
|
|
||||||
|
export function getAnimationInstance(key: AnimationInstanceKey) { |
||||||
|
return instances.get(key); |
||||||
|
} |
||||||
|
|
||||||
|
export function cancelAnimationByKey(key: AnimationInstanceKey) { |
||||||
|
const instance = getAnimationInstance(key); |
||||||
|
if(instance) { |
||||||
|
instance.isCancelled = true; |
||||||
|
instances.delete(key); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function animateSingle(tick: Function, key: AnimationInstanceKey, instance?: AnimationInstance) { |
||||||
|
if(!instance) { |
||||||
|
cancelAnimationByKey(key); |
||||||
|
instance = { isCancelled: false, deferred: deferredPromise<void>() }; |
||||||
|
instances.set(key, instance); |
||||||
|
} |
||||||
|
|
||||||
|
fastRaf(() => { |
||||||
|
if(instance.isCancelled) return; |
||||||
|
|
||||||
|
if(tick()) { |
||||||
|
animateSingle(tick, key, instance); |
||||||
|
} else { |
||||||
|
instance.deferred.resolve(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return instance.deferred; |
||||||
|
} |
||||||
|
|
||||||
|
export function animate(tick: Function) { |
||||||
|
fastRaf(() => { |
||||||
|
if(tick()) { |
||||||
|
animate(tick); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,149 @@ |
|||||||
|
// * Jolly Cobra's fastSmoothScroll slightly patched
|
||||||
|
|
||||||
|
import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck'; |
||||||
|
import { fastRaf } from './schedulers'; |
||||||
|
import { animateSingle, cancelAnimationByKey } from './animation'; |
||||||
|
import rootScope from '../lib/rootScope'; |
||||||
|
|
||||||
|
const MAX_DISTANCE = 1500; |
||||||
|
const MIN_JS_DURATION = 250; |
||||||
|
const MAX_JS_DURATION = 600; |
||||||
|
|
||||||
|
export enum FocusDirection { |
||||||
|
Up, |
||||||
|
Down, |
||||||
|
Static, |
||||||
|
}; |
||||||
|
|
||||||
|
export default function fastSmoothScroll( |
||||||
|
container: HTMLElement, |
||||||
|
element: HTMLElement, |
||||||
|
position: ScrollLogicalPosition, |
||||||
|
margin = 0, |
||||||
|
maxDistance = MAX_DISTANCE, |
||||||
|
forceDirection?: FocusDirection, |
||||||
|
forceDuration?: number, |
||||||
|
axis: 'x' | 'y' = 'y' |
||||||
|
) { |
||||||
|
//return;
|
||||||
|
|
||||||
|
if(!rootScope.settings.animationsEnabled) { |
||||||
|
forceDirection = FocusDirection.Static; |
||||||
|
} |
||||||
|
|
||||||
|
if(forceDirection === FocusDirection.Static) { |
||||||
|
forceDuration = 0; |
||||||
|
return scrollWithJs(container, element, position, margin, forceDuration, axis); |
||||||
|
/* return Promise.resolve(); |
||||||
|
|
||||||
|
element.scrollIntoView({ block: position }); |
||||||
|
|
||||||
|
cancelAnimationByKey(container); |
||||||
|
return Promise.resolve(); */ |
||||||
|
} |
||||||
|
|
||||||
|
if(axis === 'y') { |
||||||
|
const elementRect = element.getBoundingClientRect(); |
||||||
|
const containerRect = container.getBoundingClientRect(); |
||||||
|
|
||||||
|
const offsetTop = elementRect.top - containerRect.top; |
||||||
|
if(forceDirection === undefined) { |
||||||
|
if(offsetTop < -maxDistance) { |
||||||
|
container.scrollTop += (offsetTop + maxDistance); |
||||||
|
} else if(offsetTop > maxDistance) { |
||||||
|
container.scrollTop += (offsetTop - maxDistance); |
||||||
|
} |
||||||
|
} else if(forceDirection === FocusDirection.Up) { // * not tested yet
|
||||||
|
container.scrollTop = offsetTop + container.scrollTop + maxDistance; |
||||||
|
} else if(forceDirection === FocusDirection.Down) { // * not tested yet
|
||||||
|
container.scrollTop = Math.max(0, offsetTop + container.scrollTop - maxDistance); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const promise = new Promise((resolve) => { |
||||||
|
fastRaf(() => { |
||||||
|
scrollWithJs(container, element, position, margin, forceDuration, axis) |
||||||
|
.then(resolve); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
return dispatchHeavyAnimationEvent(promise); |
||||||
|
} |
||||||
|
|
||||||
|
function scrollWithJs( |
||||||
|
container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin = 0, forceDuration?: number, axis: 'x' | 'y' = 'y' |
||||||
|
) { |
||||||
|
const rectStartKey = axis === 'y' ? 'top' : 'left'; |
||||||
|
const rectEndKey = axis === 'y' ? 'bottom' : 'right'; |
||||||
|
const sizeKey = axis === 'y' ? 'height' : 'width'; |
||||||
|
const scrollSizeKey = axis === 'y' ? 'scrollHeight' : 'scrollWidth'; |
||||||
|
const scrollPositionKey = axis === 'y' ? 'scrollTop' : 'scrollLeft'; |
||||||
|
|
||||||
|
//const { offsetTop: elementTop, offsetHeight: elementHeight } = element;
|
||||||
|
const elementRect = element.getBoundingClientRect(); |
||||||
|
const containerRect = container.getBoundingClientRect(); |
||||||
|
|
||||||
|
const elementPosition = elementRect[rectStartKey] - containerRect[rectStartKey]; |
||||||
|
const elementSize = element[scrollSizeKey]; // margin is exclusive in DOMRect
|
||||||
|
|
||||||
|
const containerSize = containerRect[sizeKey]; |
||||||
|
|
||||||
|
const scrollPosition = container[scrollPositionKey]; |
||||||
|
const scrollSize = container[scrollSizeKey]; |
||||||
|
|
||||||
|
let path!: number; |
||||||
|
|
||||||
|
switch(position) { |
||||||
|
case 'start': |
||||||
|
path = (elementPosition - margin) - scrollPosition; |
||||||
|
break; |
||||||
|
case 'end': |
||||||
|
//path = (elementTop + elementHeight + margin) - containerHeight;
|
||||||
|
path = elementRect[rectEndKey] + (elementSize - elementRect[sizeKey]) - containerRect[rectEndKey]; |
||||||
|
break; |
||||||
|
// 'nearest' is not supported yet
|
||||||
|
case 'nearest': |
||||||
|
case 'center': |
||||||
|
path = elementSize < containerSize |
||||||
|
? (elementPosition + elementSize / 2) - (containerSize / 2) |
||||||
|
: elementPosition - margin; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// console.log('scrollWithJs: will scroll path:', path, element);
|
||||||
|
|
||||||
|
if(path < 0) { |
||||||
|
const remainingPath = -scrollPosition; |
||||||
|
path = Math.max(path, remainingPath); |
||||||
|
} else if(path > 0) { |
||||||
|
const remainingPath = scrollSize - (scrollPosition + containerSize); |
||||||
|
path = Math.min(path, remainingPath); |
||||||
|
} |
||||||
|
|
||||||
|
const target = container[scrollPositionKey] + path; |
||||||
|
const duration = forceDuration ?? ( |
||||||
|
MIN_JS_DURATION + (Math.abs(path) / MAX_DISTANCE) * (MAX_JS_DURATION - MIN_JS_DURATION) |
||||||
|
); |
||||||
|
const startAt = Date.now(); |
||||||
|
|
||||||
|
const tick = () => { |
||||||
|
const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1; |
||||||
|
|
||||||
|
const currentPath = path * (1 - transition(t)); |
||||||
|
container[scrollPositionKey] = Math.round(target - currentPath); |
||||||
|
|
||||||
|
return t < 1; |
||||||
|
}; |
||||||
|
|
||||||
|
if(!duration) { |
||||||
|
cancelAnimationByKey(container); |
||||||
|
tick(); |
||||||
|
return Promise.resolve(); |
||||||
|
} |
||||||
|
|
||||||
|
return animateSingle(tick, container); |
||||||
|
} |
||||||
|
|
||||||
|
function transition(t: number) { |
||||||
|
return 1 - ((1 - t) ** 3.5); |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
// * Jolly Cobra's useHeavyAnimationCheck.ts
|
||||||
|
|
||||||
|
//import { useEffect } from '../lib/teact/teact';
|
||||||
|
import { AnyToVoidFunction } from '../types'; |
||||||
|
import ListenerSetter from '../helpers/listenerSetter'; |
||||||
|
import { CancellablePromise, deferredPromise } from '../helpers/cancellablePromise'; |
||||||
|
|
||||||
|
const ANIMATION_START_EVENT = 'event-heavy-animation-start'; |
||||||
|
const ANIMATION_END_EVENT = 'event-heavy-animation-end'; |
||||||
|
|
||||||
|
let isAnimating = false; |
||||||
|
let heavyAnimationPromise: CancellablePromise<void> = Promise.resolve(); |
||||||
|
let lastAnimationPromise: Promise<any>; |
||||||
|
|
||||||
|
export const dispatchHeavyAnimationEvent = (promise: Promise<any>) => { |
||||||
|
if(!isAnimating) { |
||||||
|
heavyAnimationPromise = deferredPromise<void>(); |
||||||
|
} |
||||||
|
|
||||||
|
document.dispatchEvent(new Event(ANIMATION_START_EVENT)); |
||||||
|
isAnimating = true; |
||||||
|
lastAnimationPromise = promise; |
||||||
|
|
||||||
|
promise.then(() => { |
||||||
|
if(lastAnimationPromise !== promise) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
isAnimating = false; |
||||||
|
document.dispatchEvent(new Event(ANIMATION_END_EVENT)); |
||||||
|
heavyAnimationPromise.resolve(); |
||||||
|
}); |
||||||
|
|
||||||
|
return heavyAnimationPromise; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getHeavyAnimationPromise = () => heavyAnimationPromise; |
||||||
|
|
||||||
|
export default ( |
||||||
|
handleAnimationStart: AnyToVoidFunction, |
||||||
|
handleAnimationEnd: AnyToVoidFunction, |
||||||
|
listenerSetter?: ListenerSetter |
||||||
|
) => { |
||||||
|
//useEffect(() => {
|
||||||
|
if(isAnimating) { |
||||||
|
handleAnimationStart(); |
||||||
|
} |
||||||
|
|
||||||
|
const add = listenerSetter ? listenerSetter.add.bind(listenerSetter, document) : document.addEventListener.bind(document); |
||||||
|
const remove = listenerSetter ? listenerSetter.removeManual.bind(listenerSetter, document) : document.removeEventListener.bind(document); |
||||||
|
add(ANIMATION_START_EVENT, handleAnimationStart); |
||||||
|
add(ANIMATION_END_EVENT, handleAnimationEnd); |
||||||
|
|
||||||
|
return () => { |
||||||
|
remove(ANIMATION_END_EVENT, handleAnimationEnd); |
||||||
|
remove(ANIMATION_START_EVENT, handleAnimationStart); |
||||||
|
}; |
||||||
|
//}, [handleAnimationEnd, handleAnimationStart]);
|
||||||
|
}; |
Loading…
Reference in new issue