@egjs/axes
Version:
A module used to change the information of user action entered by various input devices such as touch screen or mouse into the logical virtual coordinates. You can easily create a UI that responds to user actions.
274 lines (260 loc) • 7.9 kB
text/typescript
/*
* Copyright (c) 2015 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import { InterruptManager } from "./InterruptManager";
import { InputType, InputTypeObserver, toAxis } from "./inputType/InputType";
import { EventManager, ChangeEventOption } from "./EventManager";
import { AxisManager, Axis } from "./AxisManager";
import { AxesOption } from "./Axes";
import {
isOutside,
getInsidePosition,
getCirculatedPos,
isEndofBounce,
} from "./Coordinate";
import { map, equal } from "./utils";
import { AnimationParam } from "./types";
import { AnimationManager } from "./animation/AnimationManager";
export class InputObserver implements InputTypeObserver {
public options: AxesOption;
private _interruptManager: InterruptManager;
private _eventManager: EventManager;
private _axisManager: AxisManager;
private _animationManager: AnimationManager;
private _isOutside = false;
private _moveDistance: Axis = null;
private _isStopped = false;
public constructor({
options,
interruptManager,
eventManager,
axisManager,
animationManager,
}: {
options: AxesOption;
interruptManager: InterruptManager;
eventManager: EventManager;
axisManager: AxisManager;
animationManager: AnimationManager;
}) {
this.options = options;
this._interruptManager = interruptManager;
this._eventManager = eventManager;
this._axisManager = axisManager;
this._animationManager = animationManager;
}
public get(input: InputType): Axis {
return this._axisManager.get(input.axes);
}
public hold(input: InputType, event) {
if (this._interruptManager.isInterrupted() || !input.axes.length) {
return;
}
const changeOption: ChangeEventOption = {
input,
event,
};
this._isStopped = false;
this._interruptManager.setInterrupt(true);
this._animationManager.stopAnimation(changeOption);
++this._eventManager.holdingCount;
if (!this._moveDistance) {
this._eventManager.hold(this._axisManager.get(), changeOption);
}
this._isOutside = this._axisManager.isOutside(input.axes);
this._moveDistance = this._axisManager.get(input.axes);
}
public change(input: InputType, event, offset: Axis, useAnimation?: boolean) {
if (
this._isStopped ||
!this._interruptManager.isInterrupting() ||
this._axisManager.every(offset, (v) => v === 0)
) {
return;
}
const nativeEvent = event.srcEvent ? event.srcEvent : event;
if (nativeEvent.__childrenAxesAlreadyChanged) {
return;
}
let depaPos: Axis = this._moveDistance || this._axisManager.get(input.axes);
let destPos: Axis;
// for outside logic
destPos = map(depaPos, (v, k) => v + (offset[k] || 0));
if (this._moveDistance) {
this._moveDistance = this._axisManager.map(
destPos,
(v, { circular, range }) =>
circular && (circular[0] || circular[1])
? getCirculatedPos(v, range, circular as boolean[])
: v
);
}
// from outside to inside
if (
this._isOutside &&
this._axisManager.every(depaPos, (v, opt) => !isOutside(v, opt.range))
) {
this._isOutside = false;
}
depaPos = this._atOutside(depaPos);
destPos = this._atOutside(destPos);
if (!this.options.nested || !this._isEndofAxis(offset, depaPos, destPos)) {
nativeEvent.__childrenAxesAlreadyChanged = true;
}
const changeOption: ChangeEventOption = {
input,
event,
};
if (useAnimation) {
const duration = this._animationManager.getDuration(destPos, depaPos);
this._animationManager.animateTo(destPos, duration, changeOption);
} else {
const isCanceled = !this._eventManager.triggerChange(
destPos,
depaPos,
changeOption,
true
);
if (isCanceled) {
this._isStopped = true;
this._moveDistance = null;
this._animationManager.finish(false);
}
}
}
public release(
input: InputType,
event,
velocity: number[],
inputDuration?: number
) {
if (
this._isStopped ||
!this._interruptManager.isInterrupting() ||
!this._moveDistance
) {
return;
}
const nativeEvent = event.srcEvent ? event.srcEvent : event;
if (nativeEvent.__childrenAxesAlreadyReleased) {
velocity = velocity.map(() => 0);
}
const pos: Axis = this._axisManager.get(input.axes);
const depaPos: Axis = this._axisManager.get();
const displacement = this._animationManager.getDisplacement(velocity);
const offset = toAxis(input.axes, displacement);
let destPos: Axis = this._axisManager.get(
this._axisManager.map(offset, (v, opt, k) => {
if (opt.circular && (opt.circular[0] || opt.circular[1])) {
return pos[k] + v;
} else {
return getInsidePosition(
pos[k] + v,
opt.range,
opt.circular as boolean[],
opt.bounce as number[]
);
}
})
);
nativeEvent.__childrenAxesAlreadyReleased = true;
const duration = this._animationManager.getDuration(
destPos,
pos,
inputDuration
);
if (duration === 0) {
destPos = { ...depaPos };
}
// prepare params
const param: AnimationParam = {
depaPos,
destPos,
duration,
delta: this._axisManager.getDelta(depaPos, destPos),
inputEvent: event,
input,
isTrusted: true,
};
--this._eventManager.holdingCount;
this._eventManager.triggerRelease(param);
if (this._eventManager.holdingCount === 0) {
this._moveDistance = null;
}
// to contol
const userWish = this._animationManager.getUserControl(param);
const isEqual = equal(userWish.destPos, depaPos);
const changeOption: ChangeEventOption = {
input,
event,
};
if (isEqual || userWish.duration === 0) {
if (!isEqual) {
this._eventManager.triggerChange(
userWish.destPos,
depaPos,
changeOption,
true
);
}
this._interruptManager.setInterrupt(false);
if (this._axisManager.isOutside()) {
this._animationManager.restore(changeOption);
} else {
this._eventManager.triggerFinish(true);
}
} else {
this._animationManager.animateTo(
userWish.destPos,
userWish.duration,
changeOption
);
}
}
// when move pointer is held in outside
private _atOutside(pos: Axis) {
if (this._isOutside) {
return this._axisManager.map(pos, (v, opt) => {
const tn = opt.range[0] - (opt.bounce[0] as number);
const tx = opt.range[1] + (opt.bounce[1] as number);
return v > tx ? tx : v < tn ? tn : v;
});
} else {
return this._axisManager.map(pos, (v, opt) => {
const min = opt.range[0];
const max = opt.range[1];
const out = opt.bounce;
const circular = opt.circular;
if ((circular[0] && v < min) || (circular[1] && v > max)) {
return v;
} else if (v < min) {
// left
return (
min - this._animationManager.interpolate(min - v, out[0] as number)
);
} else if (v > max) {
// right
return (
max + this._animationManager.interpolate(v - max, out[1] as number)
);
}
return v;
});
}
}
private _isEndofAxis(offset: Axis, depaPos: Axis, destPos: Axis) {
return this._axisManager.every(
depaPos,
(value, option, key) =>
offset[key] === 0 ||
(depaPos[key] === destPos[key] &&
isEndofBounce(
value,
option.range,
option.bounce as number[],
option.circular as boolean[]
))
);
}
}