Skip to content
This repository has been archived by the owner on Mar 31, 2023. It is now read-only.

Commit

Permalink
feat(praparat): add ability to momentum scroll
Browse files Browse the repository at this point in the history
Closes #7
  • Loading branch information
itigoore01 committed Oct 3, 2019
1 parent 5d4c92c commit 55539ad
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 18 deletions.
60 changes: 54 additions & 6 deletions projects/praparat/src/lib/pan-zoom-model.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { BehaviorSubject } from 'rxjs';
import { Point } from './point';
import { VelocityTracker } from './velocity-tracker';

export interface ZoomOptions {
initialScale: number;
initialPan: Point;
wheelZoomFactor: number;
maxScale: number;
minScale: number;
friction: number;
stopMomentumScrollThreshold: number;
}

export class PanZoomModel {

initialScale = this.options.initialScale || 1;
initialScale = this.options.initialScale;

initialPan = this.options.initialPan || { x: 0, y: 0 };
initialPan = this.options.initialPan;

wheelZoomFactor = this.options.wheelZoomFactor || 0.01;
wheelZoomFactor = this.options.wheelZoomFactor;

maxScale = this.options.maxScale || 10;
maxScale = this.options.maxScale;

minScale = this.options.minScale || 0.25;
minScale = this.options.minScale;

friction = this.options.friction;

stopMomentumScrollThreshold = this.options.stopMomentumScrollThreshold;

private velocityTracker = new VelocityTracker(100);

private _snapshot: {
pan: Point;
Expand All @@ -42,7 +51,7 @@ export class PanZoomModel {
readonly scaleObservable = this._scale$.asObservable();
readonly panObservable = this._pan$.asObservable();

constructor(private options: Partial<ZoomOptions> = {}) {
constructor(private options: ZoomOptions) {
}

pan(point: Point) {
Expand All @@ -55,6 +64,9 @@ export class PanZoomModel {

const referencePoint = this.getMiddlePoint(touches);

this.velocityTracker.reset();
this.velocityTracker.addTrackingPoint(referencePoint);

this._snapshot = {
pan: this.panPoint,
scale: this.scale,
Expand All @@ -72,6 +84,8 @@ export class PanZoomModel {
const { pan, referencePoint, scale, distance } = this._snapshot;
const point = this.getMiddlePoint(touches);

this.velocityTracker.addTrackingPoint(point);

if (touches.length > 1) {
const scaleMultiplier = this.getDistance(touches[0], touches[1]) / distance;
const newScale = this.constrainScale(scale * scaleMultiplier);
Expand All @@ -94,6 +108,38 @@ export class PanZoomModel {
this._snapshot = null;
}

createUpdateMomentam() {
let lastTick = performance.now();
let { velocityX, velocityY } = this.velocityTracker.getVelocity();
let done = false;

return () => {
let delta = performance.now() - lastTick;
lastTick = performance.now();

let { x, y } = this.panPoint;

for (; delta > 0 && !done; delta--) {
if (Math.abs(velocityX) <= this.stopMomentumScrollThreshold && Math.abs(velocityY) <= this.stopMomentumScrollThreshold) {
done = true;
}

velocityX *= this.friction;
velocityY *= this.friction;

x += velocityX;
y += velocityY;
}

this.pan({
x,
y,
});

return done;
};
}

zoom(scale: number, { focal }: { focal?: Point } = {}) {
const currentScale = this.scale;
const newScale = this.constrainScale(scale);
Expand Down Expand Up @@ -136,6 +182,8 @@ export class PanZoomModel {
maxScale: this.maxScale,
minScale: this.minScale,
wheelZoomFactor: this.wheelZoomFactor,
friction: this.friction,
stopMomentumScrollThreshold: this.stopMomentumScrollThreshold,
});
}

Expand Down
6 changes: 6 additions & 0 deletions projects/praparat/src/lib/praparat-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface PraparatConfig {
maxScale: number;
minScale: number;
willChangeDebounceTime: number;
friction: number;
stopMomentumScrollThreshold: number;
}

export function createPrapratConfig(config: Partial<PraparatConfig> = {}): PraparatConfig {
Expand All @@ -18,6 +20,8 @@ export function createPrapratConfig(config: Partial<PraparatConfig> = {}): Prapa
maxScale = 100,
minScale = 0.01,
willChangeDebounceTime = 300,
friction = 0.995,
stopMomentumScrollThreshold = 0.1,
} = config;

return {
Expand All @@ -27,6 +31,8 @@ export function createPrapratConfig(config: Partial<PraparatConfig> = {}): Prapa
maxScale,
minScale,
willChangeDebounceTime,
friction,
stopMomentumScrollThreshold,
};
}

Expand Down
50 changes: 49 additions & 1 deletion projects/praparat/src/lib/praparat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Input,
} from '@angular/core';
import { PanZoomModel } from './pan-zoom-model';
import { Subject, combineLatest, animationFrameScheduler, BehaviorSubject } from 'rxjs';
import { Subject, combineLatest, animationFrameScheduler, BehaviorSubject, Subscription } from 'rxjs';
import { takeUntil, debounceTime, throttleTime, distinctUntilChanged } from 'rxjs/operators';
import { DOCUMENT } from '@angular/common';
import { Point } from './point';
Expand Down Expand Up @@ -72,6 +72,22 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {
this.model.minScale = value;
}

@Input()
get friction() {
return this.model.friction;
}
set friction(value) {
this.model.friction = value;
}

@Input()
get stopMomentumScrollThreshold() {
return this.model.stopMomentumScrollThreshold;
}
set stopMomentumScrollThreshold(value) {
this.model.stopMomentumScrollThreshold = value;
}

private model = new PanZoomModel({
...this.defaultConfig,
});
Expand All @@ -80,6 +96,8 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {
private destroyed = new Subject<void>();
private removeListeners: (() => void)[] = [];

private animationSubscription: Subscription | null = null;

constructor(
private elementRef: ElementRef<HTMLElement>,
private renderer: Renderer2,
Expand All @@ -92,6 +110,8 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {
this.destroyed.next();
this.destroyed.complete();

this.cancelAnimation();

for (const removeListener of this.removeListeners) {
removeListener();
}
Expand All @@ -106,6 +126,7 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {
this.renderer.listen(this.elementRef.nativeElement, 'wheel', (event: WheelEvent) => {
event.preventDefault();
event.stopPropagation();
this.cancelAnimation();

const {
deltaY,
Expand All @@ -130,6 +151,7 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {
this.renderer.listen(this.elementRef.nativeElement, 'touchstart', (event: TouchEvent) => {
event.preventDefault();
event.stopPropagation();
this.cancelAnimation();

const touches: Point[] = this.touchListToPoints(event.touches);

Expand All @@ -144,6 +166,15 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {

removeTouchMoveListener();
removeTouchEndListener();

const updateMomentamPan = this.model.createUpdateMomentam();
const animationSubscription = animationFrameScheduler.schedule(function() {
if (updateMomentamPan()) {
animationSubscription.unsubscribe();
}
this.schedule();
});
this.animationSubscription = animationSubscription;
});
})
);
Expand All @@ -152,6 +183,7 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {
this.renderer.listen(this.elementRef.nativeElement, 'mousedown', (downEvent: MouseEvent) => {
downEvent.preventDefault();
downEvent.stopPropagation();
this.cancelAnimation();

this.model.touchStart([{
x: downEvent.clientX,
Expand All @@ -170,6 +202,15 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {

removeMouseMoveListener();
removeMouseUpListener();

const updateMomentamPan = this.model.createUpdateMomentam();
const animationSubscription = animationFrameScheduler.schedule(function() {
if (updateMomentamPan()) {
animationSubscription.unsubscribe();
}
this.schedule();
});
this.animationSubscription = animationSubscription;
});
})
);
Expand Down Expand Up @@ -244,6 +285,13 @@ export class PraparatComponent implements OnDestroy, AfterViewInit {
});
}

private cancelAnimation() {
if (this.animationSubscription) {
this.animationSubscription.unsubscribe();
this.animationSubscription = null;
}
}

private touchListToPoints(touchList: TouchList): Point[] {
const touches: Point[] = [];

Expand Down
21 changes: 10 additions & 11 deletions projects/praparat/src/lib/velocity-tracker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Point } from './pan-zoom-model';
import { Point } from './point';

interface VelocitySample {
timestamp: number;
Expand All @@ -22,15 +22,17 @@ export class VelocityTracker {
this.samples = [];
}

updatePoint(point: Point) {
addTrackingPoint(point: Point) {
this.samples.push({
timestamp: Date.now(),
timestamp: performance.now(),
point,
});
this.prune();
}

getVelocity(): Velocity {
this.prune();

if (this.samples.length < 2) {
return {
velocity: 0,
Expand All @@ -42,10 +44,10 @@ export class VelocityTracker {
const first = this.samples[0];
const last = this.samples[this.samples.length - 1];

const time = (last.timestamp - first.timestamp) / 1000;
const time = (last.timestamp - first.timestamp);

const distanceX = (last.point.x - first.point.y);
const distanceY = ((last.point.y - first.point.y));
const distanceX = (last.point.x - first.point.x);
const distanceY = (last.point.y - first.point.y);

return {
velocityX: distanceX / time,
Expand All @@ -55,11 +57,8 @@ export class VelocityTracker {
}

private prune() {
if (this.samples.length === 0) {
return;
}

while (this.samples[0].timestamp < (Date.now() - this.units)) {
const time = performance.now();
while (this.samples.length > 0 && time - this.samples[0].timestamp > this.units) {
this.samples.shift();
}
}
Expand Down

0 comments on commit 55539ad

Please sign in to comment.