Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

lethargy-ts

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

lethargy-ts - npm Package Compare versions

Comparing version 0.0.2 to 0.0.3

150

lib/index.esm.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc