lethargy-ts
Advanced tools
Comparing version 0.0.2 to 0.0.3
@@ -1,40 +0,57 @@ | ||
const scrollDirections = ["up", "down", "left", "right"]; | ||
/** Returns Scroll direction of the WheelEvent */ | ||
const getScrollDirection = (e) => { | ||
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) { | ||
return e.deltaX < 0 ? "left" : "right"; | ||
} | ||
return e.deltaY < 0 ? "up" : "down"; | ||
}; | ||
/** Converts default WheelEvent to our custom IWheelEvent */ | ||
const getWheelEvent = (e) => ({ | ||
deltaX: e.deltaX, | ||
deltaY: e.deltaY, | ||
deltaZ: e.deltaZ, | ||
timeStamp: e.timeStamp, | ||
}); | ||
/** Returns array of deltas of the wheel event */ | ||
const getDeltas = (e) => [e.deltaX, e.deltaY, e.deltaZ]; | ||
/** Returns module of the biggest delta of the WheelEvent */ | ||
const getBiggestDeltaModule = (e) => { | ||
const deltaModules = [e.deltaX, e.deltaY].map(Math.abs); | ||
const deltaModules = getDeltas(e).map(Math.abs); | ||
const biggestDeltaModule = Math.max(...deltaModules); | ||
return biggestDeltaModule; | ||
}; | ||
const getArrayOfNulls = (n) => new Array(n).fill(null); | ||
const generateDeltas = (n) => scrollDirections.reduce((acc, direction) => { | ||
acc[direction] = getArrayOfNulls(n); | ||
return acc; | ||
}, {}); | ||
/** Returns average of numbers in the array */ | ||
const getAverage = (arr) => { | ||
const sum = arr.reduce((acc, num) => acc + num, 0); | ||
const average = sum / arr.length; | ||
return average; | ||
/** Values below treshhold are considered 0 */ | ||
const getSign = (num, treshhold = 10) => { | ||
if (Math.abs(num) < treshhold) | ||
return 0; | ||
return Math.sign(num); | ||
}; | ||
/** Returns true if two vectors are equal */ | ||
const compareVectors = (e1, e2, treshhold = 20) => { | ||
const v1 = getDeltas(e1); | ||
const v2 = getDeltas(e2); | ||
return v1.every((vector1, index) => { | ||
const vector = v2[index]; | ||
if (vector1 < treshhold && vector < treshhold) | ||
return true; | ||
const sign1 = getSign(vector1); | ||
const sign2 = getSign(vector); | ||
return sign1 === sign2; | ||
}); | ||
}; | ||
/** If e2 event is inertia, it's delta will be no more than treshold slower */ | ||
const isAnomalyInertia = (e1, e2, treshold = 10) => { | ||
const v1 = getDeltas(e1); | ||
const v2 = getDeltas(e2); | ||
return v1.some((delta, i) => { | ||
const diff = delta - v2[i]; | ||
const maxDiff = Math.max(10, (delta * treshold) / 100); | ||
return diff > maxDiff; | ||
}); | ||
}; | ||
class Lethargy { | ||
constructor({ stability = 8, sensitivity = 100, tolerance = 0.1, delay = 150 } = {}) { | ||
this.stability = stability; | ||
this.sensitivity = sensitivity; | ||
this.tolerance = tolerance; | ||
this.delay = delay; | ||
constructor({ sensitivity = 20, inertiaDecay = 10, delay = 100 } = {}) { | ||
this.sensitivity = Math.max(1, sensitivity); | ||
this.inertiaDecay = Math.max(1, inertiaDecay); | ||
this.delay = Math.max(1, delay); | ||
// Reset inner state | ||
this.lastDeltas = generateDeltas(this.stability * 2); | ||
this.deltasTimestamp = getArrayOfNulls(this.stability * 2); | ||
this.previousEvents = []; | ||
} | ||
/** Checks whether the mousewheel event is an intent */ | ||
check(e) { | ||
var _a; | ||
const isEvent = e instanceof Event; | ||
@@ -44,39 +61,52 @@ // No event provided | ||
return null; | ||
const scrollDirection = getScrollDirection(e); | ||
const deltaModule = getBiggestDeltaModule(e); | ||
// Somehow | ||
if (deltaModule === 0) | ||
return null; | ||
// Add the new event timestamp to deltasTimestamp array, and remove the oldest entry | ||
this.deltasTimestamp.push(Date.now()); | ||
this.deltasTimestamp.shift(); | ||
const deltas = this.lastDeltas[scrollDirection]; | ||
deltas.push(deltaModule); | ||
deltas.shift(); | ||
return this.isIntentional(scrollDirection); | ||
const event = getWheelEvent(e); | ||
// DeltaModule is too small | ||
if (getBiggestDeltaModule(event) < this.sensitivity) { | ||
return false; | ||
} | ||
const isHuman = this.isHuman(event); | ||
// If event is human, reset previousEvents | ||
if (isHuman) { | ||
this.previousEvents = [event]; | ||
} | ||
// Don't push event to the previousEvents if it's timestamp is less than last seen event's timestamp | ||
else if (event.timeStamp > (((_a = this.previousEvents.at(-1)) === null || _a === void 0 ? void 0 : _a.timeStamp) || 0)) { | ||
this.previousEvents.push(event); | ||
} | ||
return isHuman; | ||
} | ||
isIntentional(scrollDirection) { | ||
// Get the relevant deltas array | ||
const deltas = this.lastDeltas[scrollDirection]; | ||
// If the array is not filled up yet, we cannot compare averages, so assume the scroll event to be intentional | ||
if (deltas[0] == null) | ||
isHuman(event) { | ||
const previousEvent = this.previousEvents.at(-1); | ||
// No previous event to compare | ||
if (!previousEvent) { | ||
return true; | ||
const prevTimestamp = this.deltasTimestamp.at(-2); | ||
// If the last mousewheel occurred within the specified delay of the penultimate one, and their values are the same. | ||
// We will assume that this is a trackpad with a constant profile | ||
if (prevTimestamp + this.delay > Date.now() && deltas[0] === deltas.at(-1)) { | ||
return false; | ||
} | ||
// Check if the new rolling average (based on the last half of the lastDeltas array) is significantly higher than the old rolling average | ||
const oldDeltas = deltas.slice(0, this.stability); | ||
const newDeltas = deltas.slice(this.stability, this.stability * 2); | ||
const oldAverage = getAverage(oldDeltas); | ||
const newAverage = getAverage(newDeltas); | ||
const newAverageIsHigher = Math.abs(newAverage * (1 + this.tolerance)) > Math.abs(oldAverage); | ||
const matchesSensitivity = Math.abs(newAverage) > this.sensitivity; | ||
if (newAverageIsHigher && matchesSensitivity) { | ||
// Enough of time passed from the last event | ||
if (event.timeStamp > previousEvent.timeStamp + this.delay) { | ||
return true; | ||
} | ||
// Add more checks here | ||
// ... | ||
const biggestDeltaModule = getBiggestDeltaModule(event); | ||
const previousBiggestDeltaModule = getBiggestDeltaModule(previousEvent); | ||
// Biggest delta module is bigger than previous delta module | ||
if (biggestDeltaModule > previousBiggestDeltaModule) { | ||
return true; | ||
} | ||
// Vectors don't match | ||
if (!compareVectors(event, previousEvent)) { | ||
return true; | ||
} | ||
// Non-decreasing deltas above 100 are likely human | ||
if (biggestDeltaModule >= 100 && biggestDeltaModule === previousBiggestDeltaModule) { | ||
return true; | ||
} | ||
const lastKnownHumanEvent = this.previousEvents[0]; | ||
// Non-decreasing deltas of known human event are likely human | ||
if (biggestDeltaModule === getBiggestDeltaModule(lastKnownHumanEvent)) { | ||
return true; | ||
} | ||
// If speed of delta's change suddenly jumped, it's likely human | ||
if (isAnomalyInertia(previousEvent, event, this.inertiaDecay)) { | ||
return true; | ||
} | ||
// No human checks passed. It's probably inertia | ||
return false; | ||
@@ -86,3 +116,3 @@ } | ||
export { Lethargy, scrollDirections }; | ||
export { Lethargy }; | ||
//# sourceMappingURL=index.esm.js.map |
149
lib/index.js
'use strict'; | ||
const scrollDirections = ["up", "down", "left", "right"]; | ||
/** Returns Scroll direction of the WheelEvent */ | ||
const getScrollDirection = (e) => { | ||
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) { | ||
return e.deltaX < 0 ? "left" : "right"; | ||
} | ||
return e.deltaY < 0 ? "up" : "down"; | ||
}; | ||
/** Converts default WheelEvent to our custom IWheelEvent */ | ||
const getWheelEvent = (e) => ({ | ||
deltaX: e.deltaX, | ||
deltaY: e.deltaY, | ||
deltaZ: e.deltaZ, | ||
timeStamp: e.timeStamp, | ||
}); | ||
/** Returns array of deltas of the wheel event */ | ||
const getDeltas = (e) => [e.deltaX, e.deltaY, e.deltaZ]; | ||
/** Returns module of the biggest delta of the WheelEvent */ | ||
const getBiggestDeltaModule = (e) => { | ||
const deltaModules = [e.deltaX, e.deltaY].map(Math.abs); | ||
const deltaModules = getDeltas(e).map(Math.abs); | ||
const biggestDeltaModule = Math.max(...deltaModules); | ||
return biggestDeltaModule; | ||
}; | ||
const getArrayOfNulls = (n) => new Array(n).fill(null); | ||
const generateDeltas = (n) => scrollDirections.reduce((acc, direction) => { | ||
acc[direction] = getArrayOfNulls(n); | ||
return acc; | ||
}, {}); | ||
/** Returns average of numbers in the array */ | ||
const getAverage = (arr) => { | ||
const sum = arr.reduce((acc, num) => acc + num, 0); | ||
const average = sum / arr.length; | ||
return average; | ||
/** Values below treshhold are considered 0 */ | ||
const getSign = (num, treshhold = 10) => { | ||
if (Math.abs(num) < treshhold) | ||
return 0; | ||
return Math.sign(num); | ||
}; | ||
/** Returns true if two vectors are equal */ | ||
const compareVectors = (e1, e2, treshhold = 20) => { | ||
const v1 = getDeltas(e1); | ||
const v2 = getDeltas(e2); | ||
return v1.every((vector1, index) => { | ||
const vector = v2[index]; | ||
if (vector1 < treshhold && vector < treshhold) | ||
return true; | ||
const sign1 = getSign(vector1); | ||
const sign2 = getSign(vector); | ||
return sign1 === sign2; | ||
}); | ||
}; | ||
/** If e2 event is inertia, it's delta will be no more than treshold slower */ | ||
const isAnomalyInertia = (e1, e2, treshold = 10) => { | ||
const v1 = getDeltas(e1); | ||
const v2 = getDeltas(e2); | ||
return v1.some((delta, i) => { | ||
const diff = delta - v2[i]; | ||
const maxDiff = Math.max(10, (delta * treshold) / 100); | ||
return diff > maxDiff; | ||
}); | ||
}; | ||
class Lethargy { | ||
constructor({ stability = 8, sensitivity = 100, tolerance = 0.1, delay = 150 } = {}) { | ||
this.stability = stability; | ||
this.sensitivity = sensitivity; | ||
this.tolerance = tolerance; | ||
this.delay = delay; | ||
constructor({ sensitivity = 20, inertiaDecay = 10, delay = 100 } = {}) { | ||
this.sensitivity = Math.max(1, sensitivity); | ||
this.inertiaDecay = Math.max(1, inertiaDecay); | ||
this.delay = Math.max(1, delay); | ||
// Reset inner state | ||
this.lastDeltas = generateDeltas(this.stability * 2); | ||
this.deltasTimestamp = getArrayOfNulls(this.stability * 2); | ||
this.previousEvents = []; | ||
} | ||
/** Checks whether the mousewheel event is an intent */ | ||
check(e) { | ||
var _a; | ||
const isEvent = e instanceof Event; | ||
@@ -46,39 +63,52 @@ // No event provided | ||
return null; | ||
const scrollDirection = getScrollDirection(e); | ||
const deltaModule = getBiggestDeltaModule(e); | ||
// Somehow | ||
if (deltaModule === 0) | ||
return null; | ||
// Add the new event timestamp to deltasTimestamp array, and remove the oldest entry | ||
this.deltasTimestamp.push(Date.now()); | ||
this.deltasTimestamp.shift(); | ||
const deltas = this.lastDeltas[scrollDirection]; | ||
deltas.push(deltaModule); | ||
deltas.shift(); | ||
return this.isIntentional(scrollDirection); | ||
const event = getWheelEvent(e); | ||
// DeltaModule is too small | ||
if (getBiggestDeltaModule(event) < this.sensitivity) { | ||
return false; | ||
} | ||
const isHuman = this.isHuman(event); | ||
// If event is human, reset previousEvents | ||
if (isHuman) { | ||
this.previousEvents = [event]; | ||
} | ||
// Don't push event to the previousEvents if it's timestamp is less than last seen event's timestamp | ||
else if (event.timeStamp > (((_a = this.previousEvents.at(-1)) === null || _a === void 0 ? void 0 : _a.timeStamp) || 0)) { | ||
this.previousEvents.push(event); | ||
} | ||
return isHuman; | ||
} | ||
isIntentional(scrollDirection) { | ||
// Get the relevant deltas array | ||
const deltas = this.lastDeltas[scrollDirection]; | ||
// If the array is not filled up yet, we cannot compare averages, so assume the scroll event to be intentional | ||
if (deltas[0] == null) | ||
isHuman(event) { | ||
const previousEvent = this.previousEvents.at(-1); | ||
// No previous event to compare | ||
if (!previousEvent) { | ||
return true; | ||
const prevTimestamp = this.deltasTimestamp.at(-2); | ||
// If the last mousewheel occurred within the specified delay of the penultimate one, and their values are the same. | ||
// We will assume that this is a trackpad with a constant profile | ||
if (prevTimestamp + this.delay > Date.now() && deltas[0] === deltas.at(-1)) { | ||
return false; | ||
} | ||
// Check if the new rolling average (based on the last half of the lastDeltas array) is significantly higher than the old rolling average | ||
const oldDeltas = deltas.slice(0, this.stability); | ||
const newDeltas = deltas.slice(this.stability, this.stability * 2); | ||
const oldAverage = getAverage(oldDeltas); | ||
const newAverage = getAverage(newDeltas); | ||
const newAverageIsHigher = Math.abs(newAverage * (1 + this.tolerance)) > Math.abs(oldAverage); | ||
const matchesSensitivity = Math.abs(newAverage) > this.sensitivity; | ||
if (newAverageIsHigher && matchesSensitivity) { | ||
// Enough of time passed from the last event | ||
if (event.timeStamp > previousEvent.timeStamp + this.delay) { | ||
return true; | ||
} | ||
// Add more checks here | ||
// ... | ||
const biggestDeltaModule = getBiggestDeltaModule(event); | ||
const previousBiggestDeltaModule = getBiggestDeltaModule(previousEvent); | ||
// Biggest delta module is bigger than previous delta module | ||
if (biggestDeltaModule > previousBiggestDeltaModule) { | ||
return true; | ||
} | ||
// Vectors don't match | ||
if (!compareVectors(event, previousEvent)) { | ||
return true; | ||
} | ||
// Non-decreasing deltas above 100 are likely human | ||
if (biggestDeltaModule >= 100 && biggestDeltaModule === previousBiggestDeltaModule) { | ||
return true; | ||
} | ||
const lastKnownHumanEvent = this.previousEvents[0]; | ||
// Non-decreasing deltas of known human event are likely human | ||
if (biggestDeltaModule === getBiggestDeltaModule(lastKnownHumanEvent)) { | ||
return true; | ||
} | ||
// If speed of delta's change suddenly jumped, it's likely human | ||
if (isAnomalyInertia(previousEvent, event, this.inertiaDecay)) { | ||
return true; | ||
} | ||
// No human checks passed. It's probably inertia | ||
return false; | ||
@@ -89,3 +119,2 @@ } | ||
exports.Lethargy = Lethargy; | ||
exports.scrollDirections = scrollDirections; | ||
//# sourceMappingURL=index.js.map |
export declare class Lethargy { | ||
/** Stability is how many records to use to calculate the average */ | ||
stability: number; | ||
/** The wheelDelta threshold. If an event has a wheelDelta below this value, it will not register */ | ||
sensitivity: number; | ||
/** How much the old rolling average have to differ from the new rolling average for it to be deemed significant */ | ||
tolerance: number; | ||
/** Threshold for the amount of time between mousewheel events for them to be deemed separate */ | ||
/** Threshold for the amount of time between wheel events for them to be deemed separate */ | ||
delay: number; | ||
private lastDeltas; | ||
private deltasTimestamp; | ||
constructor({ stability, sensitivity, tolerance, delay }?: { | ||
stability?: number | undefined; | ||
/** Max percentage decay speed of an Inertia event */ | ||
inertiaDecay: number; | ||
/** [lastKnownHumanEvent, ...inertiaEvents] */ | ||
private previousEvents; | ||
constructor({ sensitivity, inertiaDecay, delay }?: { | ||
sensitivity?: number | undefined; | ||
tolerance?: number | undefined; | ||
inertiaDecay?: number | undefined; | ||
delay?: number | undefined; | ||
@@ -20,3 +17,3 @@ }); | ||
check(e: WheelEvent): boolean | null; | ||
private isIntentional; | ||
private isHuman; | ||
} |
@@ -1,2 +0,3 @@ | ||
export declare const scrollDirections: readonly ["up", "down", "left", "right"]; | ||
export type ScrollDirection = (typeof scrollDirections)[number]; | ||
/** [deltaX, deltaY, deltaZ] */ | ||
export type Deltas = [number, number, number]; | ||
export type IWheelEvent = Pick<WheelEvent, "deltaX" | "deltaY" | "deltaZ" | "timeStamp">; |
@@ -1,9 +0,11 @@ | ||
import type { ScrollDirection } from "./types"; | ||
/** Returns Scroll direction of the WheelEvent */ | ||
export declare const getScrollDirection: (e: WheelEvent) => ScrollDirection; | ||
import type { Deltas, IWheelEvent } from "./types"; | ||
/** Converts default WheelEvent to our custom IWheelEvent */ | ||
export declare const getWheelEvent: (e: WheelEvent) => IWheelEvent; | ||
/** Returns array of deltas of the wheel event */ | ||
export declare const getDeltas: (e: IWheelEvent) => Deltas; | ||
/** Returns module of the biggest delta of the WheelEvent */ | ||
export declare const getBiggestDeltaModule: (e: WheelEvent) => number; | ||
export declare const getArrayOfNulls: (n: number) => any[]; | ||
export declare const generateDeltas: (n: number) => Record<ScrollDirection, number[]>; | ||
/** Returns average of numbers in the array */ | ||
export declare const getAverage: (arr: number[]) => number; | ||
export declare const getBiggestDeltaModule: (e: IWheelEvent) => number; | ||
/** Returns true if two vectors are equal */ | ||
export declare const compareVectors: (e1: IWheelEvent, e2: IWheelEvent, treshhold?: number) => boolean; | ||
/** If e2 event is inertia, it's delta will be no more than treshold slower */ | ||
export declare const isAnomalyInertia: (e1: IWheelEvent, e2: IWheelEvent, treshold?: number) => boolean; |
{ | ||
"name": "lethargy-ts", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Distinguish between scroll events initiated by the user, and those by inertial scrolling", | ||
@@ -42,3 +42,3 @@ "repository": "https://github.com/snelsi/lethargy-ts", | ||
"pretty-quick": "^3.1.3", | ||
"rollup": "^3.16.0", | ||
"rollup": "^3.17.0", | ||
"typescript": "^4.9.5" | ||
@@ -45,0 +45,0 @@ }, |
@@ -44,6 +44,5 @@ # ⭐ Lethargy-TS | ||
const lethargy = new Lethargy({ | ||
stability: 8, | ||
sensitivity: 100, | ||
tolerance: 0.1, | ||
delay: 150, | ||
sensitivity: 20, | ||
delay: 100, | ||
inertiaDecay: 10, | ||
}); | ||
@@ -76,10 +75,8 @@ ``` | ||
- `stability` - Specifies the length of the rolling average. In effect, the larger the value, the smoother the curve will be. This attempts to prevent anomalies from firing 'real' events. Valid values are all positive integers, but in most cases, you would need to stay between `5` and around `30`. | ||
- `sensitivity` - Specifies the minimum value for `wheelDelta` for it to register as a valid scroll event. Because the tail of the curve has low `wheelDelta` values, this will stop them from registering as valid scroll events. | ||
- `sensitivity` - Specifies the minimum value for `wheelDelta` for it to register as a valid scroll event. Because the tail of the curve has low `wheelDelta` values, this will stop them from registering as valid scroll events. The unofficial standard `wheelDelta` is `120`, so valid values are positive integers below `120`. | ||
- `delay` - Threshold for the amount of time between mouse wheel events for them to be deemed separate. | ||
- `tolerance` - Prevent small fluctuations from affecting results. Valid values are decimals from `0`, but should ideally be between `0.05` and `0.3`. | ||
- `inertiaDecay` - Inertia event may be no more than this percents smaller that previous event. | ||
- `delay` - Threshold for the amount of time between mouse wheel events for them to be deemed separate. | ||
## What problem does it solve? | ||
@@ -89,7 +86,7 @@ | ||
### How does it work? | ||
## How does it work? | ||
Lethargy keeps a record of the last few `wheelDelta` values that are passed through it, it will then work out whether these values are decreasing (decaying), and if so, concludes that the scroll event originated from inertial scrolling, and not directly from the user. | ||
### Limitations | ||
## Limitations | ||
@@ -96,0 +93,0 @@ Not all trackpads work the same, some trackpads do not have a decaying `wheelDelta` value, so our method of decay detection would not work. Instead, to cater to this situation, we had to, grudgingly, set a very small time delay between when events will register. We have tested this and normal use does not affect user experience more than usual. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
34800
263
107
1