@iam-protocol/pulse-sdk 0.2.2 → 0.2.5
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/dist/index.d.mts +2 -7
- package/dist/index.d.ts +2 -7
- package/dist/index.js +115 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +115 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/challenge/lissajous.ts +16 -4
- package/src/challenge/phrase.ts +12 -5
- package/src/config.ts +1 -0
- package/src/extraction/mfcc.ts +25 -12
- package/src/hashing/simhash.ts +9 -0
- package/src/proof/serializer.ts +6 -0
- package/src/pulse.ts +50 -8
- package/src/sensor/audio.ts +3 -2
- package/src/submit/relayer.ts +17 -3
- package/src/submit/wallet.ts +5 -1
package/dist/index.d.mts
CHANGED
|
@@ -13,6 +13,7 @@ interface PulseConfig {
|
|
|
13
13
|
cluster: "devnet" | "mainnet-beta" | "localnet";
|
|
14
14
|
rpcEndpoint?: string;
|
|
15
15
|
relayerUrl?: string;
|
|
16
|
+
relayerApiKey?: string;
|
|
16
17
|
zkeyUrl?: string;
|
|
17
18
|
wasmUrl?: string;
|
|
18
19
|
threshold?: number;
|
|
@@ -170,11 +171,6 @@ interface PackedFingerprint {
|
|
|
170
171
|
hi: bigint;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
/**
|
|
174
|
-
* Compute a 256-bit SimHash fingerprint from a feature vector.
|
|
175
|
-
* Uses deterministic random hyperplanes seeded from the protocol constant.
|
|
176
|
-
* Similar feature vectors produce fingerprints with low Hamming distance.
|
|
177
|
-
*/
|
|
178
174
|
declare function simhash(features: number[]): TemporalFingerprint;
|
|
179
175
|
/**
|
|
180
176
|
* Compute Hamming distance between two fingerprints.
|
|
@@ -316,8 +312,6 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
|
|
|
316
312
|
* Submit a proof via the IAM relayer API (walletless mode).
|
|
317
313
|
* The relayer submits the on-chain transaction using the integrator's funded account.
|
|
318
314
|
* The user needs no wallet, no SOL, no crypto knowledge.
|
|
319
|
-
*
|
|
320
|
-
* In Phase 3, the relayer endpoint is configurable (stub until executor-node in Phase 4).
|
|
321
315
|
*/
|
|
322
316
|
declare function submitViaRelayer(proof: SolanaProof, commitment: Uint8Array, options: {
|
|
323
317
|
relayerUrl: string;
|
|
@@ -360,6 +354,7 @@ declare function loadVerificationData(): StoredVerificationData | null;
|
|
|
360
354
|
/**
|
|
361
355
|
* Generate a random phonetically-balanced phrase for the voice challenge.
|
|
362
356
|
* Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
|
|
357
|
+
* Uses crypto.getRandomValues for unpredictable challenge generation.
|
|
363
358
|
*/
|
|
364
359
|
declare function generatePhrase(wordCount?: number): string;
|
|
365
360
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface PulseConfig {
|
|
|
13
13
|
cluster: "devnet" | "mainnet-beta" | "localnet";
|
|
14
14
|
rpcEndpoint?: string;
|
|
15
15
|
relayerUrl?: string;
|
|
16
|
+
relayerApiKey?: string;
|
|
16
17
|
zkeyUrl?: string;
|
|
17
18
|
wasmUrl?: string;
|
|
18
19
|
threshold?: number;
|
|
@@ -170,11 +171,6 @@ interface PackedFingerprint {
|
|
|
170
171
|
hi: bigint;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
/**
|
|
174
|
-
* Compute a 256-bit SimHash fingerprint from a feature vector.
|
|
175
|
-
* Uses deterministic random hyperplanes seeded from the protocol constant.
|
|
176
|
-
* Similar feature vectors produce fingerprints with low Hamming distance.
|
|
177
|
-
*/
|
|
178
174
|
declare function simhash(features: number[]): TemporalFingerprint;
|
|
179
175
|
/**
|
|
180
176
|
* Compute Hamming distance between two fingerprints.
|
|
@@ -316,8 +312,6 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
|
|
|
316
312
|
* Submit a proof via the IAM relayer API (walletless mode).
|
|
317
313
|
* The relayer submits the on-chain transaction using the integrator's funded account.
|
|
318
314
|
* The user needs no wallet, no SOL, no crypto knowledge.
|
|
319
|
-
*
|
|
320
|
-
* In Phase 3, the relayer endpoint is configurable (stub until executor-node in Phase 4).
|
|
321
315
|
*/
|
|
322
316
|
declare function submitViaRelayer(proof: SolanaProof, commitment: Uint8Array, options: {
|
|
323
317
|
relayerUrl: string;
|
|
@@ -360,6 +354,7 @@ declare function loadVerificationData(): StoredVerificationData | null;
|
|
|
360
354
|
/**
|
|
361
355
|
* Generate a random phonetically-balanced phrase for the voice challenge.
|
|
362
356
|
* Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
|
|
357
|
+
* Uses crypto.getRandomValues for unpredictable challenge generation.
|
|
363
358
|
*/
|
|
364
359
|
declare function generatePhrase(wordCount?: number): string;
|
|
365
360
|
/**
|
package/dist/index.js
CHANGED
|
@@ -13029,6 +13029,7 @@ var BN254_SCALAR_FIELD = BigInt(
|
|
|
13029
13029
|
var FINGERPRINT_BITS = 256;
|
|
13030
13030
|
var DEFAULT_THRESHOLD = 30;
|
|
13031
13031
|
var DEFAULT_MIN_DISTANCE = 3;
|
|
13032
|
+
var NUM_PUBLIC_INPUTS = 4;
|
|
13032
13033
|
var PROOF_A_SIZE = 64;
|
|
13033
13034
|
var PROOF_B_SIZE = 128;
|
|
13034
13035
|
var PROOF_C_SIZE = 64;
|
|
@@ -13062,6 +13063,7 @@ async function captureAudio(options = {}) {
|
|
|
13062
13063
|
}
|
|
13063
13064
|
});
|
|
13064
13065
|
const ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
|
|
13066
|
+
const capturedSampleRate = ctx.sampleRate;
|
|
13065
13067
|
const source = ctx.createMediaStreamSource(stream);
|
|
13066
13068
|
const chunks = [];
|
|
13067
13069
|
const startTime = performance.now();
|
|
@@ -13098,8 +13100,8 @@ async function captureAudio(options = {}) {
|
|
|
13098
13100
|
}
|
|
13099
13101
|
resolve({
|
|
13100
13102
|
samples,
|
|
13101
|
-
sampleRate:
|
|
13102
|
-
duration: totalLength /
|
|
13103
|
+
sampleRate: capturedSampleRate,
|
|
13104
|
+
duration: totalLength / capturedSampleRate
|
|
13103
13105
|
});
|
|
13104
13106
|
}
|
|
13105
13107
|
const maxTimer = setTimeout(stopCapture, maxDurationMs);
|
|
@@ -13314,16 +13316,29 @@ function fuseFeatures(audio, motion, touch) {
|
|
|
13314
13316
|
var FRAME_SIZE = 400;
|
|
13315
13317
|
var HOP_SIZE = 160;
|
|
13316
13318
|
var NUM_MFCC = 13;
|
|
13317
|
-
|
|
13319
|
+
var meydaModule = null;
|
|
13320
|
+
async function getMeyda() {
|
|
13321
|
+
if (!meydaModule) {
|
|
13322
|
+
try {
|
|
13323
|
+
meydaModule = await import("meyda");
|
|
13324
|
+
} catch {
|
|
13325
|
+
return null;
|
|
13326
|
+
}
|
|
13327
|
+
}
|
|
13328
|
+
return meydaModule.default ?? meydaModule;
|
|
13329
|
+
}
|
|
13330
|
+
async function extractMFCC(audio) {
|
|
13318
13331
|
const { samples, sampleRate } = audio;
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
Meyda
|
|
13322
|
-
} catch {
|
|
13332
|
+
const Meyda = await getMeyda();
|
|
13333
|
+
if (!Meyda) {
|
|
13334
|
+
console.warn("[IAM SDK] Meyda library failed to load. Audio features will be zeros.");
|
|
13323
13335
|
return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
|
|
13324
13336
|
}
|
|
13325
13337
|
const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
|
|
13326
|
-
if (numFrames < 3)
|
|
13338
|
+
if (numFrames < 3) {
|
|
13339
|
+
console.warn(`[IAM SDK] Too few audio frames (${numFrames}). Need at least 3.`);
|
|
13340
|
+
return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
|
|
13341
|
+
}
|
|
13327
13342
|
const mfccFrames = [];
|
|
13328
13343
|
for (let i = 0; i < numFrames; i++) {
|
|
13329
13344
|
const start = i * HOP_SIZE;
|
|
@@ -13339,7 +13354,7 @@ function extractMFCC(audio) {
|
|
|
13339
13354
|
mfccFrames.push(features2.mfcc);
|
|
13340
13355
|
}
|
|
13341
13356
|
}
|
|
13342
|
-
if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4).fill(0);
|
|
13357
|
+
if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
|
|
13343
13358
|
const deltaFrames = computeDeltas(mfccFrames);
|
|
13344
13359
|
const deltaDeltaFrames = computeDeltas(deltaFrames);
|
|
13345
13360
|
const features = [];
|
|
@@ -13489,10 +13504,16 @@ function getHyperplanes(dimension) {
|
|
|
13489
13504
|
cachedDimension = dimension;
|
|
13490
13505
|
return planes;
|
|
13491
13506
|
}
|
|
13507
|
+
var EXPECTED_FEATURE_DIMENSION = 259;
|
|
13492
13508
|
function simhash(features) {
|
|
13493
13509
|
if (features.length === 0) {
|
|
13494
13510
|
return new Array(FINGERPRINT_BITS).fill(0);
|
|
13495
13511
|
}
|
|
13512
|
+
if (features.length !== EXPECTED_FEATURE_DIMENSION) {
|
|
13513
|
+
console.warn(
|
|
13514
|
+
`[IAM SDK] Feature vector has ${features.length} dimensions, expected ${EXPECTED_FEATURE_DIMENSION}. Fingerprint quality may be degraded.`
|
|
13515
|
+
);
|
|
13516
|
+
}
|
|
13496
13517
|
const planes = getHyperplanes(features.length);
|
|
13497
13518
|
const fingerprint = [];
|
|
13498
13519
|
for (let i = 0; i < FINGERPRINT_BITS; i++) {
|
|
@@ -13588,6 +13609,11 @@ function negateG1Y(yDecStr) {
|
|
|
13588
13609
|
return toBigEndian32(yNeg.toString());
|
|
13589
13610
|
}
|
|
13590
13611
|
function serializeProof(proof, publicSignals) {
|
|
13612
|
+
if (publicSignals.length !== NUM_PUBLIC_INPUTS) {
|
|
13613
|
+
throw new Error(
|
|
13614
|
+
`Expected ${NUM_PUBLIC_INPUTS} public signals, got ${publicSignals.length}`
|
|
13615
|
+
);
|
|
13616
|
+
}
|
|
13591
13617
|
const a0 = toBigEndian32(proof.pi_a[0]);
|
|
13592
13618
|
const a1 = negateG1Y(proof.pi_a[1]);
|
|
13593
13619
|
const proofA = new Uint8Array(PROOF_A_SIZE);
|
|
@@ -13710,7 +13736,10 @@ async function submitViaWallet(proof, commitment, options) {
|
|
|
13710
13736
|
systemProgram: SystemProgram13.programId
|
|
13711
13737
|
}).rpc();
|
|
13712
13738
|
const anchorIdl = await anchor.Program.fetchIdl(anchorProgramId, provider);
|
|
13713
|
-
if (anchorIdl) {
|
|
13739
|
+
if (!anchorIdl) {
|
|
13740
|
+
return { success: false, error: "Failed to fetch IAM Anchor program IDL" };
|
|
13741
|
+
}
|
|
13742
|
+
{
|
|
13714
13743
|
const anchorProgram = new anchor.Program(anchorIdl, provider);
|
|
13715
13744
|
if (options.isFirstVerification) {
|
|
13716
13745
|
const [identityPda] = PublicKey23.findProgramAddressSync(
|
|
@@ -13765,6 +13794,7 @@ async function submitViaWallet(proof, commitment, options) {
|
|
|
13765
13794
|
}
|
|
13766
13795
|
|
|
13767
13796
|
// src/submit/relayer.ts
|
|
13797
|
+
var RELAYER_TIMEOUT_MS = 3e4;
|
|
13768
13798
|
async function submitViaRelayer(proof, commitment, options) {
|
|
13769
13799
|
try {
|
|
13770
13800
|
const body = {
|
|
@@ -13779,21 +13809,31 @@ async function submitViaRelayer(proof, commitment, options) {
|
|
|
13779
13809
|
if (options.apiKey) {
|
|
13780
13810
|
headers["X-API-Key"] = options.apiKey;
|
|
13781
13811
|
}
|
|
13812
|
+
const controller = new AbortController();
|
|
13813
|
+
const timer = setTimeout(() => controller.abort(), RELAYER_TIMEOUT_MS);
|
|
13782
13814
|
const response = await fetch(options.relayerUrl, {
|
|
13783
13815
|
method: "POST",
|
|
13784
13816
|
headers,
|
|
13785
|
-
body: JSON.stringify(body)
|
|
13817
|
+
body: JSON.stringify(body),
|
|
13818
|
+
signal: controller.signal
|
|
13786
13819
|
});
|
|
13820
|
+
clearTimeout(timer);
|
|
13787
13821
|
if (!response.ok) {
|
|
13788
13822
|
const errorText = await response.text();
|
|
13789
13823
|
return { success: false, error: `Relayer error: ${response.status} ${errorText}` };
|
|
13790
13824
|
}
|
|
13791
13825
|
const result = await response.json();
|
|
13826
|
+
if (result.success !== true) {
|
|
13827
|
+
return { success: false, error: "Relayer returned unsuccessful response" };
|
|
13828
|
+
}
|
|
13792
13829
|
return {
|
|
13793
|
-
success:
|
|
13830
|
+
success: true,
|
|
13794
13831
|
txSignature: result.tx_signature
|
|
13795
13832
|
};
|
|
13796
13833
|
} catch (err) {
|
|
13834
|
+
if (err.name === "AbortError") {
|
|
13835
|
+
return { success: false, error: "Relayer request timed out" };
|
|
13836
|
+
}
|
|
13797
13837
|
return { success: false, error: err.message ?? String(err) };
|
|
13798
13838
|
}
|
|
13799
13839
|
}
|
|
@@ -13849,14 +13889,39 @@ function loadVerificationData() {
|
|
|
13849
13889
|
var inMemoryStore = null;
|
|
13850
13890
|
|
|
13851
13891
|
// src/pulse.ts
|
|
13852
|
-
function extractFeatures(data) {
|
|
13853
|
-
const audioFeatures = data.audio ? extractMFCC(data.audio) : new Array(169).fill(0);
|
|
13892
|
+
async function extractFeatures(data) {
|
|
13893
|
+
const audioFeatures = data.audio ? await extractMFCC(data.audio) : new Array(169).fill(0);
|
|
13854
13894
|
const motionFeatures = extractMotionFeatures(data.motion);
|
|
13855
13895
|
const touchFeatures = extractTouchFeatures(data.touch);
|
|
13856
13896
|
return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
|
|
13857
13897
|
}
|
|
13898
|
+
var MIN_AUDIO_SAMPLES = 16e3;
|
|
13899
|
+
var MIN_MOTION_SAMPLES = 10;
|
|
13900
|
+
var MIN_TOUCH_SAMPLES = 10;
|
|
13858
13901
|
async function processSensorData(sensorData, config, wallet, connection) {
|
|
13859
|
-
const
|
|
13902
|
+
const audioSamples = sensorData.audio?.samples.length ?? 0;
|
|
13903
|
+
const motionSamples = sensorData.motion.length;
|
|
13904
|
+
const touchSamples = sensorData.touch.length;
|
|
13905
|
+
const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
|
|
13906
|
+
const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
|
|
13907
|
+
const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
|
|
13908
|
+
if (!hasAudio && !hasMotion && !hasTouch) {
|
|
13909
|
+
return {
|
|
13910
|
+
success: false,
|
|
13911
|
+
commitment: new Uint8Array(32),
|
|
13912
|
+
isFirstVerification: true,
|
|
13913
|
+
error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture."
|
|
13914
|
+
};
|
|
13915
|
+
}
|
|
13916
|
+
if (!hasAudio) {
|
|
13917
|
+
return {
|
|
13918
|
+
success: false,
|
|
13919
|
+
commitment: new Uint8Array(32),
|
|
13920
|
+
isFirstVerification: true,
|
|
13921
|
+
error: "No voice data detected. Please speak the phrase clearly during capture."
|
|
13922
|
+
};
|
|
13923
|
+
}
|
|
13924
|
+
const features = await extractFeatures(sensorData);
|
|
13860
13925
|
const fingerprint = simhash(features);
|
|
13861
13926
|
const tbh = await generateTBH(fingerprint);
|
|
13862
13927
|
const previousData = loadVerificationData();
|
|
@@ -13867,15 +13932,23 @@ async function processSensorData(sensorData, config, wallet, connection) {
|
|
|
13867
13932
|
fingerprint: previousData.fingerprint,
|
|
13868
13933
|
salt: BigInt(previousData.salt),
|
|
13869
13934
|
commitment: BigInt(previousData.commitment),
|
|
13870
|
-
commitmentBytes:
|
|
13935
|
+
commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
|
|
13871
13936
|
};
|
|
13872
13937
|
const circuitInput = prepareCircuitInput(
|
|
13873
13938
|
tbh,
|
|
13874
13939
|
previousTBH,
|
|
13875
13940
|
config.threshold
|
|
13876
13941
|
);
|
|
13877
|
-
const wasmPath = config.wasmUrl
|
|
13878
|
-
const zkeyPath = config.zkeyUrl
|
|
13942
|
+
const wasmPath = config.wasmUrl;
|
|
13943
|
+
const zkeyPath = config.zkeyUrl;
|
|
13944
|
+
if (!wasmPath || !zkeyPath) {
|
|
13945
|
+
return {
|
|
13946
|
+
success: false,
|
|
13947
|
+
commitment: tbh.commitmentBytes,
|
|
13948
|
+
isFirstVerification: false,
|
|
13949
|
+
error: "wasmUrl and zkeyUrl must be configured for re-verification proof generation"
|
|
13950
|
+
};
|
|
13951
|
+
}
|
|
13879
13952
|
const { proof, publicSignals } = await generateProof(
|
|
13880
13953
|
circuitInput,
|
|
13881
13954
|
wasmPath,
|
|
@@ -13902,7 +13975,7 @@ async function processSensorData(sensorData, config, wallet, connection) {
|
|
|
13902
13975
|
submission = await submitViaRelayer(
|
|
13903
13976
|
solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
|
|
13904
13977
|
tbh.commitmentBytes,
|
|
13905
|
-
{ relayerUrl: config.relayerUrl, isFirstVerification }
|
|
13978
|
+
{ relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
|
|
13906
13979
|
);
|
|
13907
13980
|
} else {
|
|
13908
13981
|
return {
|
|
@@ -14188,14 +14261,18 @@ var SYLLABLES = [
|
|
|
14188
14261
|
"su",
|
|
14189
14262
|
"tu"
|
|
14190
14263
|
];
|
|
14264
|
+
function secureRandom(max) {
|
|
14265
|
+
const arr = new Uint32Array(1);
|
|
14266
|
+
crypto.getRandomValues(arr);
|
|
14267
|
+
return arr[0] % max;
|
|
14268
|
+
}
|
|
14191
14269
|
function generatePhrase(wordCount = 5) {
|
|
14192
14270
|
const words = [];
|
|
14193
14271
|
for (let w = 0; w < wordCount; w++) {
|
|
14194
|
-
const syllableCount = 2 +
|
|
14272
|
+
const syllableCount = 2 + secureRandom(2);
|
|
14195
14273
|
let word = "";
|
|
14196
14274
|
for (let s = 0; s < syllableCount; s++) {
|
|
14197
|
-
|
|
14198
|
-
word += SYLLABLES[idx];
|
|
14275
|
+
word += SYLLABLES[secureRandom(SYLLABLES.length)];
|
|
14199
14276
|
}
|
|
14200
14277
|
words.push(word);
|
|
14201
14278
|
}
|
|
@@ -14212,10 +14289,10 @@ function generatePhraseSequence(count = 3, wordCount = 4) {
|
|
|
14212
14289
|
];
|
|
14213
14290
|
const words = [];
|
|
14214
14291
|
for (let w = 0; w < wordCount; w++) {
|
|
14215
|
-
const syllableCount = 2 +
|
|
14292
|
+
const syllableCount = 2 + secureRandom(2);
|
|
14216
14293
|
let word = "";
|
|
14217
14294
|
for (let s = 0; s < syllableCount; s++) {
|
|
14218
|
-
word += subset[
|
|
14295
|
+
word += subset[secureRandom(subset.length)];
|
|
14219
14296
|
}
|
|
14220
14297
|
words.push(word);
|
|
14221
14298
|
}
|
|
@@ -14233,11 +14310,13 @@ function randomLissajousParams() {
|
|
|
14233
14310
|
[3, 5],
|
|
14234
14311
|
[4, 5]
|
|
14235
14312
|
];
|
|
14236
|
-
const
|
|
14313
|
+
const arr = new Uint32Array(2);
|
|
14314
|
+
crypto.getRandomValues(arr);
|
|
14315
|
+
const pair = ratios[arr[0] % ratios.length];
|
|
14237
14316
|
return {
|
|
14238
14317
|
a: pair[0],
|
|
14239
14318
|
b: pair[1],
|
|
14240
|
-
delta: Math.PI * (0.25 +
|
|
14319
|
+
delta: Math.PI * (0.25 + arr[1] / 4294967295 * 0.5),
|
|
14241
14320
|
points: 200
|
|
14242
14321
|
};
|
|
14243
14322
|
}
|
|
@@ -14266,14 +14345,22 @@ function generateLissajousSequence(count = 2) {
|
|
|
14266
14345
|
[3, 7],
|
|
14267
14346
|
[4, 7]
|
|
14268
14347
|
];
|
|
14269
|
-
const shuffled = [...allRatios]
|
|
14348
|
+
const shuffled = [...allRatios];
|
|
14349
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
14350
|
+
const arr = new Uint32Array(1);
|
|
14351
|
+
crypto.getRandomValues(arr);
|
|
14352
|
+
const j = arr[0] % (i + 1);
|
|
14353
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
14354
|
+
}
|
|
14270
14355
|
const sequence = [];
|
|
14271
14356
|
for (let i = 0; i < count; i++) {
|
|
14272
14357
|
const pair = shuffled[i % shuffled.length];
|
|
14358
|
+
const deltaArr = new Uint32Array(1);
|
|
14359
|
+
crypto.getRandomValues(deltaArr);
|
|
14273
14360
|
const params = {
|
|
14274
14361
|
a: pair[0],
|
|
14275
14362
|
b: pair[1],
|
|
14276
|
-
delta: Math.PI * (0.1 +
|
|
14363
|
+
delta: Math.PI * (0.1 + deltaArr[0] / 4294967295 * 0.8),
|
|
14277
14364
|
points: 200
|
|
14278
14365
|
};
|
|
14279
14366
|
sequence.push({ params, points: generateLissajousPoints(params) });
|