perf(scroll): filter velocity using exponential moving window

This commit is contained in:
Manu Mtz.-Almeida
2018-08-07 16:04:13 +02:00
parent f9447355b9
commit 419ef7b836
2 changed files with 46 additions and 72 deletions

View File

@ -5,7 +5,6 @@ export interface ScrollBaseDetail {
} }
export interface ScrollDetail extends GestureDetail, ScrollBaseDetail { export interface ScrollDetail extends GestureDetail, ScrollBaseDetail {
positions: number[];
scrollTop: number; scrollTop: number;
scrollLeft: number; scrollLeft: number;
} }

View File

@ -13,9 +13,30 @@ export class Scroll {
private watchDog: any; private watchDog: any;
private isScrolling = false; private isScrolling = false;
private lastScroll = 0; private lastScroll = 0;
private detail: ScrollDetail;
private queued = false; private queued = false;
// Detail is used in a hot loop in the scroll event, by allocating it here
// V8 will be able to inline any read/write to it since it's a monomorphic class.
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
private detail: ScrollDetail = {
scrollTop: 0,
scrollLeft: 0,
type: 'scroll',
event: undefined!,
startX: 0,
startY: 0,
startTimeStamp: 0,
currentX: 0,
currentY: 0,
velocityX: 0,
velocityY: 0,
deltaX: 0,
deltaY: 0,
timeStamp: 0,
data: undefined,
isScrolling: true,
};
@Element() el!: HTMLElement; @Element() el!: HTMLElement;
@Prop({ context: 'config' }) config!: Config; @Prop({ context: 'config' }) config!: Config;
@ -51,31 +72,6 @@ export class Scroll {
*/ */
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>; @Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
constructor() {
// Detail is used in a hot loop in the scroll event, by allocating it here
// V8 will be able to inline any read/write to it since it's a monomorphic class.
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
this.detail = {
positions: [],
scrollTop: 0,
scrollLeft: 0,
type: 'scroll',
event: undefined!,
startX: 0,
startY: 0,
startTimeStamp: 0,
currentX: 0,
currentY: 0,
velocityX: 0,
velocityY: 0,
deltaX: 0,
deltaY: 0,
timeStamp: 0,
data: undefined,
isScrolling: true,
};
}
componentWillLoad() { componentWillLoad() {
if (this.forceOverscroll === undefined) { if (this.forceOverscroll === undefined) {
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in this.win); this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in this.win);
@ -116,10 +112,7 @@ export class Scroll {
/** Scroll to the bottom of the component */ /** Scroll to the bottom of the component */
@Method() @Method()
scrollToBottom(duration: number): Promise<void> { scrollToBottom(duration: number): Promise<void> {
const y = (this.el) const y = this.el.scrollHeight - this.el.clientHeight;
? this.el.scrollHeight - this.el.clientHeight
: 0;
return this.scrollToPoint(0, y, duration); return this.scrollToPoint(0, y, duration);
} }
@ -209,9 +202,8 @@ export class Scroll {
// chill out for a frame first // chill out for a frame first
this.queue.write(() => { this.queue.write(() => {
this.queue.write(timeStamp => { this.queue.write(timeStamp => {
// TODO: review stencilt type of timeStamp startTime = timeStamp;
startTime = timeStamp!; step(timeStamp);
step(timeStamp!);
}); });
}); });
@ -263,49 +255,32 @@ export class Scroll {
function updateScrollDetail( function updateScrollDetail(
detail: ScrollDetail, detail: ScrollDetail,
el: HTMLElement, el: HTMLElement,
timeStamp: number, timestamp: number,
didStart: boolean didStart: boolean
) { ) {
const scrollTop = el.scrollTop; const prevX = detail.currentX;
const scrollLeft = el.scrollLeft; const prevY = detail.currentY;
const positions = detail.positions; const prevT = detail.timeStamp;
const currentX = el.scrollLeft;
const currentY = el.scrollTop;
if (didStart) { if (didStart) {
// remember the start positions // remember the start positions
detail.startTimeStamp = timeStamp; detail.startTimeStamp = timestamp;
detail.startY = scrollTop; detail.startX = currentX;
detail.startX = scrollLeft; detail.startY = currentY;
positions.length = 0; detail.velocityX = detail.velocityY = 0;
} }
detail.timeStamp = timestamp;
detail.currentX = detail.scrollLeft = currentX;
detail.currentY = detail.scrollTop = currentY;
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
detail.timeStamp = timeStamp; const timeDelta = timestamp - prevT;
detail.currentY = detail.scrollTop = scrollTop; if (timeDelta > 0 && timeDelta < 100) {
detail.currentX = detail.scrollLeft = scrollLeft; const velocityX = (currentX - prevX) / timeDelta;
detail.deltaY = scrollTop - detail.startY; const velocityY = (currentY - prevY) / timeDelta;
detail.deltaX = scrollLeft - detail.startX; detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
// actively scrolling
positions.push(scrollTop, scrollLeft, timeStamp);
// move pointer to position measured 100ms ago
const timeRange = timeStamp - 100;
let startPos = positions.length - 1;
while (startPos > 0 && positions[startPos] > timeRange) {
startPos -= 3;
}
if (startPos > 3) {
// compute relative movement between these two points
const frequency = 1 / (positions[startPos] - timeStamp);
const movedX = positions[startPos - 1] - scrollLeft;
const movedY = positions[startPos - 2] - scrollTop;
// based on XXms compute the movement to apply for each render step
// velocity = space/time = s*(1/t) = s*frequency
detail.velocityX = movedX * frequency;
detail.velocityY = movedY * frequency;
} else {
detail.velocityX = 0;
detail.velocityY = 0;
} }
} }