@daboss2003/liveness-web 1.0.3 → 1.0.4
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/ui.js +76 -15
- package/package.json +1 -1
- package/src/ui.ts +77 -15
package/dist/ui.js
CHANGED
|
@@ -79,6 +79,44 @@ function createStyles() {
|
|
|
79
79
|
-webkit-appearance: none;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
.lv-root.lv-is-loading .lv-ring-wrap,
|
|
83
|
+
.lv-root.lv-is-loading .lv-dots,
|
|
84
|
+
.lv-root.lv-is-loading .lv-instruction,
|
|
85
|
+
.lv-root.lv-is-loading .lv-pos-hint,
|
|
86
|
+
.lv-root.lv-is-loading .lv-hint-icon {
|
|
87
|
+
opacity: 0;
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
}
|
|
90
|
+
.lv-root:not(.lv-is-loading) .lv-loading { display: none; }
|
|
91
|
+
.lv-loading {
|
|
92
|
+
position: absolute;
|
|
93
|
+
z-index: 2;
|
|
94
|
+
left: 50%;
|
|
95
|
+
top: ${OVAL_TOP_PCT}%;
|
|
96
|
+
transform: translate(-50%, -50%);
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 8px;
|
|
101
|
+
text-align: center;
|
|
102
|
+
color: var(--lv-white);
|
|
103
|
+
text-shadow: 0 1px 6px rgba(0,0,0,0.5);
|
|
104
|
+
}
|
|
105
|
+
.lv-spinner {
|
|
106
|
+
width: 32px;
|
|
107
|
+
height: 32px;
|
|
108
|
+
border: 3px solid rgba(255,255,255,0.25);
|
|
109
|
+
border-top-color: var(--lv-white);
|
|
110
|
+
border-radius: 50%;
|
|
111
|
+
animation: lv-spin 0.9s linear infinite;
|
|
112
|
+
}
|
|
113
|
+
.lv-loading-text {
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
opacity: 0.9;
|
|
117
|
+
}
|
|
118
|
+
@keyframes lv-spin { to { transform: rotate(360deg); } }
|
|
119
|
+
|
|
82
120
|
/* ── Dark overlay with oval cutout ──────────────────────────────────── */
|
|
83
121
|
.lv-overlay {
|
|
84
122
|
position: absolute;
|
|
@@ -269,7 +307,7 @@ export function startLivenessWithUI(options) {
|
|
|
269
307
|
const container = options.container ?? document.body;
|
|
270
308
|
// ── Root shell ─────────────────────────────────────────────────────────────
|
|
271
309
|
const root = document.createElement("div");
|
|
272
|
-
root.className = "lv-root";
|
|
310
|
+
root.className = "lv-root lv-is-loading";
|
|
273
311
|
root.appendChild(createStyles());
|
|
274
312
|
// ── Video background ───────────────────────────────────────────────────────
|
|
275
313
|
const videoBg = document.createElement("div");
|
|
@@ -305,6 +343,14 @@ export function startLivenessWithUI(options) {
|
|
|
305
343
|
hintIcon.className = "lv-hint-icon";
|
|
306
344
|
hintIcon.setAttribute("aria-hidden", "true");
|
|
307
345
|
root.appendChild(hintIcon);
|
|
346
|
+
// ── Loading spinner (shown until model is ready) ───────────────────────────
|
|
347
|
+
const loading = document.createElement("div");
|
|
348
|
+
loading.className = "lv-loading";
|
|
349
|
+
loading.innerHTML = `
|
|
350
|
+
<div class="lv-spinner" aria-hidden="true"></div>
|
|
351
|
+
<div class="lv-loading-text">Preparing camera...</div>
|
|
352
|
+
`;
|
|
353
|
+
root.appendChild(loading);
|
|
308
354
|
// ── Header ─────────────────────────────────────────────────────────────────
|
|
309
355
|
// const header = document.createElement("div");
|
|
310
356
|
// header.className = "lv-header";
|
|
@@ -336,6 +382,8 @@ export function startLivenessWithUI(options) {
|
|
|
336
382
|
const ringEl = ringWrap.querySelector(".lv-ring-progress");
|
|
337
383
|
const dots = Array.from(dotsEl.querySelectorAll(".lv-dot"));
|
|
338
384
|
const P = ELLIPSE_PERIMETER;
|
|
385
|
+
let isLoading = true;
|
|
386
|
+
let pendingChallenge = null;
|
|
339
387
|
// ── UI helpers ──────────────────────────────────────────────────────────────
|
|
340
388
|
function setProgress(completedSteps) {
|
|
341
389
|
if (!ringEl)
|
|
@@ -368,6 +416,21 @@ export function startLivenessWithUI(options) {
|
|
|
368
416
|
posHint.classList.toggle("visible", !inside);
|
|
369
417
|
posHint.textContent = inside ? "" : (reason ?? "Move your face into the oval");
|
|
370
418
|
}
|
|
419
|
+
function renderChallenge(stepIndex, stepLabel) {
|
|
420
|
+
if (stepIndex === -1) {
|
|
421
|
+
setProgress(LIVENESS_STEP_COUNT);
|
|
422
|
+
setCapturePulse();
|
|
423
|
+
setHint(null);
|
|
424
|
+
setDots(LIVENESS_STEP_COUNT);
|
|
425
|
+
instruction.textContent = stepLabel;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
setProgress(stepIndex);
|
|
429
|
+
setDots(stepIndex);
|
|
430
|
+
setHint(stepLabel);
|
|
431
|
+
instruction.textContent = stepLabel;
|
|
432
|
+
ringEl?.classList.remove("lv-ring-pulse");
|
|
433
|
+
}
|
|
371
434
|
function cleanup() {
|
|
372
435
|
engine.stop();
|
|
373
436
|
video.removeEventListener("playing", onVideoPlaying);
|
|
@@ -393,20 +456,13 @@ export function startLivenessWithUI(options) {
|
|
|
393
456
|
sounds,
|
|
394
457
|
callbacks: {
|
|
395
458
|
onChallengeChanged: (stepIndex, stepLabel) => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return;
|
|
459
|
+
pendingChallenge = { stepIndex, stepLabel };
|
|
460
|
+
if (!isLoading) {
|
|
461
|
+
renderChallenge(stepIndex, stepLabel);
|
|
462
|
+
}
|
|
463
|
+
if (stepIndex !== -1) {
|
|
464
|
+
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
403
465
|
}
|
|
404
|
-
setProgress(stepIndex);
|
|
405
|
-
setDots(stepIndex);
|
|
406
|
-
setHint(stepLabel);
|
|
407
|
-
instruction.textContent = stepLabel;
|
|
408
|
-
ringEl?.classList.remove("lv-ring-pulse");
|
|
409
|
-
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
410
466
|
},
|
|
411
467
|
onFaceInOval: (inside, reason) => {
|
|
412
468
|
setFaceInOval(inside, reason);
|
|
@@ -429,7 +485,12 @@ export function startLivenessWithUI(options) {
|
|
|
429
485
|
setProgress(0);
|
|
430
486
|
setDots(0);
|
|
431
487
|
setHint(null);
|
|
432
|
-
engine.start().then(() => {
|
|
488
|
+
engine.start().then(() => {
|
|
489
|
+
isLoading = false;
|
|
490
|
+
root.classList.remove("lv-is-loading");
|
|
491
|
+
if (pendingChallenge)
|
|
492
|
+
renderChallenge(pendingChallenge.stepIndex, pendingChallenge.stepLabel);
|
|
493
|
+
}, (err) => {
|
|
433
494
|
cleanup();
|
|
434
495
|
const reason = err instanceof LivenessError
|
|
435
496
|
? err.code
|
package/package.json
CHANGED
package/src/ui.ts
CHANGED
|
@@ -94,6 +94,44 @@ function createStyles(): HTMLStyleElement {
|
|
|
94
94
|
-webkit-appearance: none;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
.lv-root.lv-is-loading .lv-ring-wrap,
|
|
98
|
+
.lv-root.lv-is-loading .lv-dots,
|
|
99
|
+
.lv-root.lv-is-loading .lv-instruction,
|
|
100
|
+
.lv-root.lv-is-loading .lv-pos-hint,
|
|
101
|
+
.lv-root.lv-is-loading .lv-hint-icon {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
}
|
|
105
|
+
.lv-root:not(.lv-is-loading) .lv-loading { display: none; }
|
|
106
|
+
.lv-loading {
|
|
107
|
+
position: absolute;
|
|
108
|
+
z-index: 2;
|
|
109
|
+
left: 50%;
|
|
110
|
+
top: ${OVAL_TOP_PCT}%;
|
|
111
|
+
transform: translate(-50%, -50%);
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
align-items: center;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
text-align: center;
|
|
117
|
+
color: var(--lv-white);
|
|
118
|
+
text-shadow: 0 1px 6px rgba(0,0,0,0.5);
|
|
119
|
+
}
|
|
120
|
+
.lv-spinner {
|
|
121
|
+
width: 32px;
|
|
122
|
+
height: 32px;
|
|
123
|
+
border: 3px solid rgba(255,255,255,0.25);
|
|
124
|
+
border-top-color: var(--lv-white);
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
animation: lv-spin 0.9s linear infinite;
|
|
127
|
+
}
|
|
128
|
+
.lv-loading-text {
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
font-weight: 500;
|
|
131
|
+
opacity: 0.9;
|
|
132
|
+
}
|
|
133
|
+
@keyframes lv-spin { to { transform: rotate(360deg); } }
|
|
134
|
+
|
|
97
135
|
/* ── Dark overlay with oval cutout ──────────────────────────────────── */
|
|
98
136
|
.lv-overlay {
|
|
99
137
|
position: absolute;
|
|
@@ -289,7 +327,7 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
289
327
|
|
|
290
328
|
// ── Root shell ─────────────────────────────────────────────────────────────
|
|
291
329
|
const root = document.createElement("div");
|
|
292
|
-
root.className = "lv-root";
|
|
330
|
+
root.className = "lv-root lv-is-loading";
|
|
293
331
|
root.appendChild(createStyles());
|
|
294
332
|
|
|
295
333
|
// ── Video background ───────────────────────────────────────────────────────
|
|
@@ -330,6 +368,15 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
330
368
|
hintIcon.setAttribute("aria-hidden", "true");
|
|
331
369
|
root.appendChild(hintIcon);
|
|
332
370
|
|
|
371
|
+
// ── Loading spinner (shown until model is ready) ───────────────────────────
|
|
372
|
+
const loading = document.createElement("div");
|
|
373
|
+
loading.className = "lv-loading";
|
|
374
|
+
loading.innerHTML = `
|
|
375
|
+
<div class="lv-spinner" aria-hidden="true"></div>
|
|
376
|
+
<div class="lv-loading-text">Preparing camera...</div>
|
|
377
|
+
`;
|
|
378
|
+
root.appendChild(loading);
|
|
379
|
+
|
|
333
380
|
// ── Header ─────────────────────────────────────────────────────────────────
|
|
334
381
|
// const header = document.createElement("div");
|
|
335
382
|
// header.className = "lv-header";
|
|
@@ -367,6 +414,8 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
367
414
|
const ringEl = ringWrap.querySelector(".lv-ring-progress") as SVGEllipseElement | null;
|
|
368
415
|
const dots = Array.from(dotsEl.querySelectorAll(".lv-dot"));
|
|
369
416
|
const P = ELLIPSE_PERIMETER;
|
|
417
|
+
let isLoading = true;
|
|
418
|
+
let pendingChallenge: { stepIndex: number; stepLabel: string } | null = null;
|
|
370
419
|
|
|
371
420
|
// ── UI helpers ──────────────────────────────────────────────────────────────
|
|
372
421
|
|
|
@@ -402,6 +451,22 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
402
451
|
posHint.textContent = inside ? "" : (reason ?? "Move your face into the oval");
|
|
403
452
|
}
|
|
404
453
|
|
|
454
|
+
function renderChallenge(stepIndex: number, stepLabel: string): void {
|
|
455
|
+
if (stepIndex === -1) {
|
|
456
|
+
setProgress(LIVENESS_STEP_COUNT);
|
|
457
|
+
setCapturePulse();
|
|
458
|
+
setHint(null);
|
|
459
|
+
setDots(LIVENESS_STEP_COUNT);
|
|
460
|
+
instruction.textContent = stepLabel;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
setProgress(stepIndex);
|
|
464
|
+
setDots(stepIndex);
|
|
465
|
+
setHint(stepLabel);
|
|
466
|
+
instruction.textContent = stepLabel;
|
|
467
|
+
ringEl?.classList.remove("lv-ring-pulse");
|
|
468
|
+
}
|
|
469
|
+
|
|
405
470
|
function cleanup(): void {
|
|
406
471
|
engine.stop();
|
|
407
472
|
video.removeEventListener("playing", onVideoPlaying);
|
|
@@ -430,20 +495,13 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
430
495
|
sounds,
|
|
431
496
|
callbacks: {
|
|
432
497
|
onChallengeChanged: (stepIndex, stepLabel) => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return;
|
|
498
|
+
pendingChallenge = { stepIndex, stepLabel };
|
|
499
|
+
if (!isLoading) {
|
|
500
|
+
renderChallenge(stepIndex, stepLabel);
|
|
501
|
+
}
|
|
502
|
+
if (stepIndex !== -1) {
|
|
503
|
+
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
440
504
|
}
|
|
441
|
-
setProgress(stepIndex);
|
|
442
|
-
setDots(stepIndex);
|
|
443
|
-
setHint(stepLabel);
|
|
444
|
-
instruction.textContent = stepLabel;
|
|
445
|
-
ringEl?.classList.remove("lv-ring-pulse");
|
|
446
|
-
options.callbacks.onChallengeChanged?.(stepIndex, stepLabel);
|
|
447
505
|
},
|
|
448
506
|
onFaceInOval: (inside, reason) => {
|
|
449
507
|
setFaceInOval(inside, reason);
|
|
@@ -469,7 +527,11 @@ export function startLivenessWithUI(options: StartLivenessOptions): LivenessEngi
|
|
|
469
527
|
setHint(null);
|
|
470
528
|
|
|
471
529
|
engine.start().then(
|
|
472
|
-
() => {
|
|
530
|
+
() => {
|
|
531
|
+
isLoading = false;
|
|
532
|
+
root.classList.remove("lv-is-loading");
|
|
533
|
+
if (pendingChallenge) renderChallenge(pendingChallenge.stepIndex, pendingChallenge.stepLabel);
|
|
534
|
+
},
|
|
473
535
|
(err) => {
|
|
474
536
|
cleanup();
|
|
475
537
|
const reason =
|