@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.
Files changed (3) hide show
  1. package/dist/ui.js +76 -15
  2. package/package.json +1 -1
  3. 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
- if (stepIndex === -1) {
397
- setProgress(LIVENESS_STEP_COUNT);
398
- setCapturePulse();
399
- setHint(null);
400
- setDots(LIVENESS_STEP_COUNT);
401
- instruction.textContent = stepLabel;
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(() => { }, (err) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daboss2003/liveness-web",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Web liveness detection using MediaPipe Face Landmarker (CDN)",
5
5
  "keywords": [
6
6
  "liveness",
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
- if (stepIndex === -1) {
434
- setProgress(LIVENESS_STEP_COUNT);
435
- setCapturePulse();
436
- setHint(null);
437
- setDots(LIVENESS_STEP_COUNT);
438
- instruction.textContent = stepLabel;
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 =