@gkucmierz/utils 5.4.1 → 5.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.mjs CHANGED
@@ -38,6 +38,9 @@ import {
38
38
  import {
39
39
  consumeIteratorNonBlocking
40
40
  } from './src/developer-utils/consume-iterator.mjs'
41
+ import {
42
+ EASING_PRESETS, createInterruptibleEase, estimateEasingParams, fitEasing, solveBezier
43
+ } from './src/developer-utils/easing-matcher.mjs'
41
44
  import {
42
45
 
43
46
  } from './src/developer-utils/get-type.mjs'
@@ -169,6 +172,7 @@ export * from './src/data-structures/heap.mjs';
169
172
  export * from './src/data-structures/list-node.mjs';
170
173
  export * from './src/developer-utils/binary-search.mjs';
171
174
  export * from './src/developer-utils/consume-iterator.mjs';
175
+ export * from './src/developer-utils/easing-matcher.mjs';
172
176
  export * from './src/developer-utils/get-type.mjs';
173
177
  export * from './src/developer-utils/grid-navigation.mjs';
174
178
  export * from './src/developer-utils/measure-performance.mjs';
@@ -210,5 +214,5 @@ export * from './src/string-arrays/parse-heading.mjs';
210
214
  export * from './src/string-arrays/slugify.mjs';
211
215
 
212
216
  export default [
213
- createLangtonsAnt, createUnlimitedGrid, combinations, combinationsIterator, bin2gray, gray2bin, nChooseK, permutations, permutationsIterator, ConflatingQueue, SetCnt, Stack, Trie, Heap, ListNode, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking, detectGridColumns, navigateGrid, measurePerformance, memoize, naturalSearch, QUORUM_CONFIG, QUORUM_DEFAULT_K, QUORUM_DEFAULT_N, QUORUM_MAX_RELATIVE_ERROR, QUORUM_MAX_RELATIVE_ERROR_PCT, QUORUM_ZERO_FLOOR_EPSILON, computeRelativeError, evaluateQuorum, evaluateQuorum4of5, isQuorumEqual, randNormal, array2range, range2array, setSafeInterval, Type, Types, is, isCallable, of, same, barycentricCoordinates, applyScreenDragQuat, axisAngleToMatrix4, axisAngleToQuat, crossProduct, dotProduct, eulerYXZToQuat, getRotationMatrixFromVectors, matToEulerYXZ, matToQuat, multiplyMatrix4, multiplyQuat, normalize, normalizeQuat, projectToTrackball, quatToMat, rotateVecByQuat, slerp, matrixAsArray, OrthoSet, containsPoint1D, containsPointNd, diffBox, diffRange, formatRange, measureBox, measureRange, mutualBox, mutualRange, parseRange, range, sharedMeasureNd, sumBox, sumRange, xorBox, xorRange, egcd, factors, factorsBI, gcd, gcdBI, lcm, lcmBI, lucasLehmerBI, mobius, mobiusBI, mod, modBI, phi, phiBI, powMod, powModBI, tonelliShanksBI, nelderMead, particleSwarmOptimization, simulatedAnnealing, goldenRatio, goldenRatioBI, goldenRatioStr, gpn, gpnBI, heronsFormula, heronsFormulaBI, squareRoot, squareRootBI, arrayHistogram, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, chunks, chunksAsyncIterator, chunksIterator, copyCase, formatBigNumber, formatBigNumberBI, wrapFn, parseHeading, slugify
217
+ createLangtonsAnt, createUnlimitedGrid, combinations, combinationsIterator, bin2gray, gray2bin, nChooseK, permutations, permutationsIterator, ConflatingQueue, SetCnt, Stack, Trie, Heap, ListNode, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking, EASING_PRESETS, createInterruptibleEase, estimateEasingParams, fitEasing, solveBezier, detectGridColumns, navigateGrid, measurePerformance, memoize, naturalSearch, QUORUM_CONFIG, QUORUM_DEFAULT_K, QUORUM_DEFAULT_N, QUORUM_MAX_RELATIVE_ERROR, QUORUM_MAX_RELATIVE_ERROR_PCT, QUORUM_ZERO_FLOOR_EPSILON, computeRelativeError, evaluateQuorum, evaluateQuorum4of5, isQuorumEqual, randNormal, array2range, range2array, setSafeInterval, Type, Types, is, isCallable, of, same, barycentricCoordinates, applyScreenDragQuat, axisAngleToMatrix4, axisAngleToQuat, crossProduct, dotProduct, eulerYXZToQuat, getRotationMatrixFromVectors, matToEulerYXZ, matToQuat, multiplyMatrix4, multiplyQuat, normalize, normalizeQuat, projectToTrackball, quatToMat, rotateVecByQuat, slerp, matrixAsArray, OrthoSet, containsPoint1D, containsPointNd, diffBox, diffRange, formatRange, measureBox, measureRange, mutualBox, mutualRange, parseRange, range, sharedMeasureNd, sumBox, sumRange, xorBox, xorRange, egcd, factors, factorsBI, gcd, gcdBI, lcm, lcmBI, lucasLehmerBI, mobius, mobiusBI, mod, modBI, phi, phiBI, powMod, powModBI, tonelliShanksBI, nelderMead, particleSwarmOptimization, simulatedAnnealing, goldenRatio, goldenRatioBI, goldenRatioStr, gpn, gpnBI, heronsFormula, heronsFormulaBI, squareRoot, squareRootBI, arrayHistogram, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, chunks, chunksAsyncIterator, chunksIterator, copyCase, formatBigNumber, formatBigNumberBI, wrapFn, parseHeading, slugify
214
218
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gkucmierz/utils",
3
- "version": "5.4.1",
3
+ "version": "5.4.2",
4
4
  "type": "module",
5
5
  "description": "Usefull functions for solving programming tasks",
6
6
  "keywords": [
@@ -70,6 +70,7 @@
70
70
  "test": "jasmine",
71
71
  "watch": "nodemon --exec 'npm test'",
72
72
  "docs": "npm run docs:build; npm run docs:open",
73
+ "build": "npm run build:main",
73
74
  "build:main": "node scripts/generate_main.mjs && node scripts/generate_instacode_links.mjs",
74
75
  "docs:build": "jsdoc -c jsdoc.json && cp -r docs/@gkucmierz/utils/*/* docs/ && rm -rf docs/@gkucmierz",
75
76
  "docs:open": "open docs/index.html",
@@ -83,6 +84,9 @@
83
84
  "homepage": "https://gitea.7u.pl/gkucmierz/utils",
84
85
  "author": "Grzegorz Kućmierz",
85
86
  "license": "MIT",
87
+ "publishConfig": {
88
+ "access": "public"
89
+ },
86
90
  "devDependencies": {
87
91
  "@eslint/js": "^10.0.1",
88
92
  "docdash": "^2.0.2",
@@ -0,0 +1,227 @@
1
+ import { is } from './type.mjs';
2
+
3
+ const normalizeEasingSamples = samples => {
4
+ if (!is(samples, Array) || samples.length < 2) return null;
5
+ const first = samples[0];
6
+ if (!first) return null;
7
+
8
+ // Cumulative { t, y } mode when neither dt/diffT nor dy/diff are present
9
+ const isCumulative =
10
+ first.t !== undefined &&
11
+ first.y !== undefined &&
12
+ first.dt === undefined &&
13
+ first.diffT === undefined &&
14
+ first.dy === undefined &&
15
+ first.diff === undefined;
16
+
17
+ if (isCumulative) {
18
+ const t0 = samples[0].t < 0 ? samples[0].t : 0;
19
+ const y0 = samples[0].y < 0 ? samples[0].y : 0;
20
+ const spanT = samples[samples.length - 1].t - t0;
21
+ const spanY = samples[samples.length - 1].y - y0;
22
+ if (!spanT || !spanY) return null;
23
+ return samples.map(s => ({
24
+ t: (s.t - t0) / spanT,
25
+ y: (s.y - y0) / spanY
26
+ }));
27
+ }
28
+
29
+ let sumT = 0;
30
+ let sumY = 0;
31
+ const pts = samples.map(s => {
32
+ const dt = s.dt !== undefined ? s.dt : (s.diffT || 0);
33
+ const dy = s.dy !== undefined ? s.dy : (s.diff || 0);
34
+ sumT += dt;
35
+ sumY += dy;
36
+ return { t: sumT, y: sumY };
37
+ });
38
+ if (!sumT || !sumY) return null;
39
+ return pts.map(p => ({ t: p.t / sumT, y: p.y / sumY }));
40
+ };
41
+
42
+ /**
43
+ * Solves Cubic Bezier curve for given control points using Newton-Raphson iteration
44
+ * with bisection fallback for steep/flat segments.
45
+ * @param {number} p1x
46
+ * @param {number} p1y
47
+ * @param {number} p2x
48
+ * @param {number} p2y
49
+ * @returns {function(number): number}
50
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IM5QDYDcCmAhHALxBxgQF8EAzGKMBAIgAEBzAawFcBjMEmQgPQc4IDKgYBuYMHTZ8RPlKA|▶ Try it live in Instacode}
51
+ */
52
+ export const solveBezier = (p1x, p1y, p2x, p2y) => t => {
53
+ if (t <= 0) return 0;
54
+ if (t >= 1) return 1;
55
+ if (p1x === p1y && p2x === p2y) return t;
56
+
57
+ const cx = 3 * p1x;
58
+ const bx = 3 * (p2x - p1x) - cx;
59
+ const ax = 1 - cx - bx;
60
+ const cy = 3 * p1y;
61
+ const by = 3 * (p2y - p1y) - cy;
62
+ const ay = 1 - cy - by;
63
+
64
+ const sampleX = u => ((ax * u + bx) * u + cx) * u;
65
+ const sampleDerivX = u => (3 * ax * u + 2 * bx) * u + cx;
66
+ const sampleY = u => ((ay * u + by) * u + cy) * u;
67
+
68
+ let u = t;
69
+ for (let i = 0; i < 8; i++) {
70
+ const xErr = sampleX(u) - t;
71
+ if (Math.abs(xErr) < 1e-6) return sampleY(u);
72
+ const d = sampleDerivX(u);
73
+ if (Math.abs(d) < 1e-6) break;
74
+ u -= xErr / d;
75
+ }
76
+
77
+ // Bisection fallback when derivative is near zero or Newton steps out of [0, 1]
78
+ let lo = 0;
79
+ let hi = 1;
80
+ u = Math.max(0, Math.min(1, t));
81
+ for (let i = 0; i < 12; i++) {
82
+ const x = sampleX(u);
83
+ if (Math.abs(x - t) < 1e-6) break;
84
+ if (t > x) lo = u;
85
+ else hi = u;
86
+ u = 0.5 * (lo + hi);
87
+ }
88
+ return sampleY(u);
89
+ };
90
+
91
+ /**
92
+ * Standard CSS easing presets and Chromium quintic deceleration curve.
93
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IKIEEDKBJAcgcQH0AFAJVU1QBVMEBfBAMxijAQCIABAcwGsBXAMZgQAUxgAvAPT84IADYBndgG5gwDDgIlylGmqA|▶ Try it live in Instacode}
94
+ */
95
+ export const EASING_PRESETS = Object.freeze({
96
+ 'ease-in-out': solveBezier(0.42, 0, 0.58, 1.0),
97
+ 'ease-out': solveBezier(0, 0, 0.58, 1.0),
98
+ 'ease': solveBezier(0.25, 0.1, 0.25, 1.0),
99
+ 'linear': t => t,
100
+ 'ease-in': solveBezier(0.42, 0, 1.0, 1.0),
101
+ 'quint-out': t => 1 - Math.pow(1 - t, 5),
102
+ });
103
+
104
+ /**
105
+ * Matches telemetry samples against standard CSS easing presets.
106
+ * Supports both frame delta samples ({ dy, dt } / { diff, diffT }) and cumulative points ({ t, y }).
107
+ * Returns sorted list of preset matches with RMSE and percentage score.
108
+ * @param {Array<Object>} samples
109
+ * @returns {Array<{ name: string, match: number, rmse: number }>}
110
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IGYjgUQIYGcQB2A5ggL5oxRgIBEAAkQNYCuAxmCAKYwBeA9MzggANrhoBuYMHRY8hIpKA|▶ Try it live in Instacode}
111
+ */
112
+ export function fitEasing(samples) {
113
+ const norm = normalizeEasingSamples(samples);
114
+ if (!norm) return [];
115
+
116
+ return Object.entries(EASING_PRESETS).map(([name, fn]) => {
117
+ const err = norm.reduce((acc, p) => acc + Math.pow(p.y - fn(p.t), 2), 0);
118
+ const rmse = Math.sqrt(err / norm.length);
119
+ const match = Math.max(0, Math.min(100, Math.round((1 - rmse * 2) * 1000) / 10));
120
+ return { name, match, rmse: Math.round(rmse * 1000) / 1000 };
121
+ }).sort((a, b) => b.match - a.match || a.rmse - b.rmse);
122
+ }
123
+
124
+ /**
125
+ * Estimates optimal cubic-bezier control points and asymmetry metrics for raw telemetry samples.
126
+ * Uses a 2-stage coarse-to-fine search over (x1, y1, x2, y2) in [0, 1]^4.
127
+ * @param {Array<Object>} samples
128
+ * @returns {?{ asymmetryRatio: number, midpointT: number, cssBezier: string, controlPoints: Array<number>, matchPercentage: number, evaluate: function(number): number }}
129
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IKYGc7gIZ1QUW3RADsBzABWxmzHQQF8EAzGKMBAIgAEyBrAK4BjMCFQwAXgHoBWADbpOAbmDAMWMLgJFSlarXQqgA|▶ Try it live in Instacode}
130
+ */
131
+ export function estimateEasingParams(samples) {
132
+ if (!is(samples, Array) || samples.length < 3) return null;
133
+ const norm = normalizeEasingSamples(samples);
134
+ if (!norm) return null;
135
+
136
+ const midPoint = norm.find(p => p.y >= 0.5) || { t: 0.5 };
137
+ const asymmetry = Math.round(((1 - midPoint.t) / (midPoint.t || 1e-4)) * 10) / 10;
138
+
139
+ const evalRmse = (x1, y1, x2, y2) => {
140
+ const fn = solveBezier(x1, y1, x2, y2);
141
+ let err = 0;
142
+ for (let i = 0; i < norm.length; i++) {
143
+ const d = norm[i].y - fn(norm[i].t);
144
+ err += d * d;
145
+ }
146
+ return Math.sqrt(err / norm.length);
147
+ };
148
+
149
+ let best = { rmse: evalRmse(0.25, 0.1, 0.25, 1.0), x1: 0.25, y1: 0.1, x2: 0.25, y2: 1.0 };
150
+
151
+ // Seed with known CSS presets first
152
+ const presetSeeds = [
153
+ [0.25, 0.1, 0.25, 1.0],
154
+ [0.42, 0.0, 0.58, 1.0],
155
+ [0.0, 0.0, 0.58, 1.0],
156
+ [0.42, 0.0, 1.0, 1.0],
157
+ [0.25, 0.25, 0.75, 0.75],
158
+ [0.22, 1.0, 0.36, 1.0]
159
+ ];
160
+ for (const [x1, y1, x2, y2] of presetSeeds) {
161
+ const rmse = evalRmse(x1, y1, x2, y2);
162
+ if (rmse < best.rmse) best = { rmse, x1, y1, x2, y2 };
163
+ }
164
+
165
+ // Stage 1: Coarse grid over (x1, y1, x2, y2)
166
+ for (let x1 = 0.05; x1 <= 0.65; x1 += 0.15) {
167
+ for (let y1 = 0.0; y1 <= 1.0; y1 += 0.2) {
168
+ for (let x2 = 0.15; x2 <= 0.95; x2 += 0.2) {
169
+ for (let y2 = 0.6; y2 <= 1.0; y2 += 0.2) {
170
+ const rmse = evalRmse(x1, y1, x2, y2);
171
+ if (rmse < best.rmse) best = { rmse, x1, y1, x2, y2 };
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // Stage 2: Local coordinate refinement around best candidate
178
+ for (const step of [0.05, 0.02]) {
179
+ let improved = true;
180
+ while (improved) {
181
+ improved = false;
182
+ const coords = ['x1', 'y1', 'x2', 'y2'];
183
+ for (const key of coords) {
184
+ for (const dir of [-step, step]) {
185
+ const cand = { ...best, [key]: Math.max(0, Math.min(1, best[key] + dir)) };
186
+ const rmse = evalRmse(cand.x1, cand.y1, cand.x2, cand.y2);
187
+ if (rmse + 1e-6 < best.rmse) {
188
+ best = { rmse, x1: cand.x1, y1: cand.y1, x2: cand.x2, y2: cand.y2 };
189
+ improved = true;
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ const rx1 = Number(best.x1.toFixed(2));
197
+ const ry1 = Number(best.y1.toFixed(2));
198
+ const rx2 = Number(best.x2.toFixed(2));
199
+ const ry2 = Number(best.y2.toFixed(2));
200
+ const match = Math.max(0, Math.min(100, Math.round((1 - best.rmse * 2) * 1000) / 10));
201
+ const cssBezier = `cubic-bezier(${rx1.toFixed(2)}, ${ry1.toFixed(2)}, ${rx2.toFixed(2)}, ${ry2.toFixed(2)})`;
202
+
203
+ return {
204
+ asymmetryRatio: asymmetry,
205
+ midpointT: Math.round(midPoint.t * 100) / 100,
206
+ cssBezier,
207
+ controlPoints: [rx1, ry1, rx2, ry2],
208
+ matchPercentage: match,
209
+ evaluate: solveBezier(rx1, ry1, rx2, ry2)
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Creates interruptible easing curve preserving kinetic momentum (C1 continuous).
215
+ * @param {number} vCurr Current velocity in pixels/second
216
+ * @param {number} targetDist Remaining distance to target in pixels
217
+ * @param {number} duration Total duration in milliseconds
218
+ * @returns {function(number): number}
219
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMYwKYEM4YJIB2uMMArhHCAEYA2GAolgM4YIC+CAZjFGAgEQABAOYBrUqjAgMMAF4B6UpRpN+AbmDB02XIWJkK1OoxYagA|▶ Try it live in Instacode}
220
+ */
221
+ export function createInterruptibleEase(vCurr, targetDist, duration) {
222
+ if (!targetDist || duration <= 0) return t => t;
223
+ const slope = (vCurr * (duration / 1000)) / targetDist;
224
+ const p1x = 0.25;
225
+ const p1y = Math.min(1.0, Math.max(0, slope * p1x));
226
+ return solveBezier(p1x, p1y, 0.25, 1.0);
227
+ }
@@ -159,7 +159,7 @@ const computeClusterConsensusValue = (values) => {
159
159
  * - `success: true` is returned **if and only if** the largest mutually-equal clique $C^*$ has size $|C^*| \ge K$
160
160
  * (e.g., at least $4$ out of $5$ datapoints are mutually equal within $0.01\%$).
161
161
  *
162
- * @param {Array<number|number[]|{source?: string, id?: string, value: number|number[]}>} datapoints - Input datapoints.
162
+ * @param {Array<(number|Array<number>|Object)>} datapoints - Input datapoints.
163
163
  * @param {Object} [options={}] - Quorum configuration options.
164
164
  * @param {number} [options.k=4] - Minimum number of mutually-equal datapoints required for success ($K$).
165
165
  * @param {number} [options.n=5] - Expected total number of independent sources ($N$).
@@ -332,7 +332,7 @@ export const evaluateQuorum = (
332
332
  * Convenience helper for the standard $4\text{-of-}5$ quorum check with $0.01\%$ (`0.0001`)
333
333
  * maximum relative error tolerance.
334
334
  *
335
- * @param {Array<number|number[]|{source?: string, id?: string, value: number|number[]}>} datapoints - 5 source datapoints.
335
+ * @param {Array<(number|Array<number>|Object)>} datapoints - 5 source datapoints.
336
336
  * @param {Object} [options={}] - Optional overrides (`maxRelativeError`, `strictN`, `zeroFloor`).
337
337
  * @returns {Object} Quorum evaluation result.
338
338
  * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IKYDcCGAbArpuVARV1lzABYoAzAVgQF8FqYowEAiAAQHMBrXAGMwIVDABeAelxwQ2AM4cA3MGAYc+QiTKUatFUA|▶ Try it live in Instacode}