samlax12 commited on
Commit
a3aa6eb
·
verified ·
1 Parent(s): 6a1b44a

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +273 -169
index.html CHANGED
@@ -1,9 +1,9 @@
1
  <!DOCTYPE html>
2
- <html lang="en">
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>AR Nail Studio</title>
7
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
@@ -15,7 +15,6 @@
15
  box-sizing: border-box;
16
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
17
  }
18
-
19
  body {
20
  background-color: #f8f8f8;
21
  overflow: hidden;
@@ -24,18 +23,22 @@
24
  height: 100%;
25
  touch-action: none;
26
  }
27
-
28
  .container {
29
  position: relative;
30
  width: 100%;
31
  height: 100%;
32
  overflow: hidden;
33
  }
34
-
35
  .input_video {
36
- display: none;
 
 
 
 
 
 
 
37
  }
38
-
39
  .output_canvas {
40
  position: absolute;
41
  width: 100%;
@@ -44,7 +47,6 @@
44
  top: 0;
45
  object-fit: cover;
46
  }
47
-
48
  .controls-container {
49
  position: absolute;
50
  bottom: 0;
@@ -54,16 +56,13 @@
54
  background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
55
  z-index: 10;
56
  }
57
-
58
  .controls-content {
59
  max-width: 500px;
60
  margin: 0 auto;
61
  }
62
-
63
  .control-section {
64
  margin-bottom: 25px;
65
  }
66
-
67
  .section-title {
68
  color: white;
69
  font-size: 14px;
@@ -72,7 +71,6 @@
72
  letter-spacing: 0.5px;
73
  text-transform: uppercase;
74
  }
75
-
76
  .design-options {
77
  display: flex;
78
  overflow-x: auto;
@@ -80,11 +78,9 @@
80
  gap: 12px;
81
  scrollbar-width: none;
82
  }
83
-
84
  .design-options::-webkit-scrollbar {
85
  display: none;
86
  }
87
-
88
  .design-option {
89
  width: 60px;
90
  height: 60px;
@@ -95,25 +91,21 @@
95
  transition: all 0.2s ease;
96
  flex-shrink: 0;
97
  }
98
-
99
  .design-option.selected {
100
  border-color: #fff;
101
  transform: scale(1.05);
102
  box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
103
  }
104
-
105
  .slider-container {
106
  margin: 15px 0;
107
  color: white;
108
  }
109
-
110
  .slider-label {
111
  display: flex;
112
  justify-content: space-between;
113
  font-size: 12px;
114
  margin-bottom: 8px;
115
  }
116
-
117
  .slider {
118
  -webkit-appearance: none;
119
  appearance: none;
@@ -123,7 +115,6 @@
123
  border-radius: 2px;
124
  outline: none;
125
  }
126
-
127
  .slider::-webkit-slider-thumb {
128
  -webkit-appearance: none;
129
  appearance: none;
@@ -134,24 +125,20 @@
134
  cursor: pointer;
135
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
136
  }
137
-
138
  .toggle-section {
139
  display: flex;
140
  justify-content: space-between;
141
  align-items: center;
142
  }
143
-
144
  .toggle-container {
145
  display: flex;
146
  align-items: center;
147
  }
148
-
149
  .toggle-label {
150
  color: white;
151
  font-size: 14px;
152
  margin-right: 10px;
153
  }
154
-
155
  .toggle {
156
  position: relative;
157
  width: 46px;
@@ -160,11 +147,9 @@
160
  background-color: rgba(255, 255, 255, 0.3);
161
  transition: all 0.3s ease;
162
  }
163
-
164
  .toggle.active {
165
  background-color: #3a86ff;
166
  }
167
-
168
  .toggle-handle {
169
  position: absolute;
170
  top: 2px;
@@ -176,11 +161,9 @@
176
  transition: all 0.3s ease;
177
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
178
  }
179
-
180
  .toggle.active .toggle-handle {
181
  left: 24px;
182
  }
183
-
184
  .snap-button {
185
  position: absolute;
186
  bottom: 240px;
@@ -198,11 +181,9 @@
198
  border: none;
199
  outline: none;
200
  }
201
-
202
  .snap-button:active {
203
  transform: translateX(-50%) scale(0.95);
204
  }
205
-
206
  .snap-button::after {
207
  content: '';
208
  width: 52px;
@@ -210,7 +191,6 @@
210
  border-radius: 50%;
211
  border: 2px solid #f8f8f8;
212
  }
213
-
214
  .camera-flip {
215
  position: absolute;
216
  top: 20px;
@@ -227,7 +207,6 @@
227
  z-index: 20;
228
  font-size: 18px;
229
  }
230
-
231
  .loading-screen {
232
  position: fixed;
233
  top: 0;
@@ -242,7 +221,6 @@
242
  z-index: 9999;
243
  transition: opacity 0.5s ease;
244
  }
245
-
246
  .loading-spinner {
247
  width: 50px;
248
  height: 50px;
@@ -252,19 +230,16 @@
252
  animation: spin 1s linear infinite;
253
  margin-bottom: 20px;
254
  }
255
-
256
  .loading-text {
257
  color: white;
258
  font-size: 18px;
259
  font-weight: 500;
260
  }
261
-
262
  @keyframes spin {
263
  to {
264
  transform: rotate(360deg);
265
  }
266
  }
267
-
268
  .app-title {
269
  position: absolute;
270
  top: 20px;
@@ -280,20 +255,81 @@
280
  width: 100%;
281
  height: 100%;
282
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  </style>
284
  </head>
285
 
286
  <body>
287
  <div class="loading-screen">
288
  <div class="loading-spinner"></div>
289
- <div class="loading-text">Loading AR Nail Studio...</div>
 
 
 
 
 
 
290
  </div>
291
 
 
 
292
  <div class="container">
293
  <video class="input_video" playsinline></video>
294
  <canvas class="output_canvas"></canvas>
295
 
296
- <div class="app-title">AR Nail Studio</div>
297
 
298
  <button class="camera-flip">
299
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -309,7 +345,7 @@
309
  <div class="controls-container">
310
  <div class="controls-content">
311
  <div class="control-section">
312
- <div class="section-title">Nail Design</div>
313
  <div class="design-options">
314
  <div class="design-option selected" data-design="pink">
315
  <div class="color-block" style="background: linear-gradient(45deg, #ff9a9e, #fad0c4);"></div>
@@ -333,11 +369,11 @@
333
  </div>
334
 
335
  <div class="control-section">
336
- <div class="section-title">Adjustments</div>
337
 
338
  <div class="slider-container">
339
  <div class="slider-label">
340
- <span>Size</span>
341
  <span id="size-value">100%</span>
342
  </div>
343
  <input type="range" min="50" max="150" value="100" class="slider" id="size-slider">
@@ -345,7 +381,7 @@
345
 
346
  <div class="slider-container">
347
  <div class="slider-label">
348
- <span>Length</span>
349
  <span id="length-value">100%</span>
350
  </div>
351
  <input type="range" min="80" max="200" value="100" class="slider" id="length-slider">
@@ -353,7 +389,7 @@
353
 
354
  <div class="slider-container">
355
  <div class="slider-label">
356
- <span>Opacity</span>
357
  <span id="opacity-value">100%</span>
358
  </div>
359
  <input type="range" min="30" max="100" value="100" class="slider" id="opacity-slider">
@@ -362,14 +398,14 @@
362
 
363
  <div class="control-section toggle-section">
364
  <div class="toggle-container">
365
- <span class="toggle-label">Realistic Shadows</span>
366
  <div class="toggle active" id="shadow-toggle">
367
  <div class="toggle-handle"></div>
368
  </div>
369
  </div>
370
 
371
  <div class="toggle-container">
372
- <span class="toggle-label">Show Hand Lines</span>
373
  <div class="toggle" id="lines-toggle">
374
  <div class="toggle-handle"></div>
375
  </div>
@@ -380,7 +416,7 @@
380
  </div>
381
 
382
  <script>
383
- // Define nail colors and their properties
384
  const nailDesigns = {
385
  pink: {
386
  color: 'linear-gradient(45deg, #ff9a9e, #fad0c4)',
@@ -407,31 +443,40 @@
407
  shadowColor: 'rgba(35, 37, 38, 0.5)'
408
  }
409
  };
410
-
411
- // Hide loading screen when page is ready
412
- document.addEventListener('DOMContentLoaded', function() {
413
- document.querySelector('.loading-screen').style.opacity = 0;
414
- setTimeout(() => {
415
- document.querySelector('.loading-screen').style.display = 'none';
416
- }, 500);
417
- });
418
-
419
- // Initialize variables
420
  let currentDesign = 'pink';
421
  let sizeScale = 1.0;
422
  let lengthScale = 1.0;
423
- let opacity = 1.0; // Fixed variable name to avoid duplicate
424
  let showShadows = true;
425
  let showLines = false;
426
- let facingMode = 'environment'; // 'user' = front camera, 'environment' = back camera
427
-
428
- // Get UI elements
 
 
 
 
 
 
 
 
 
 
 
 
429
  const videoElement = document.getElementsByClassName('input_video')[0];
430
  const canvasElement = document.getElementsByClassName('output_canvas')[0];
 
 
 
 
431
  canvasElement.width = window.innerWidth;
432
  canvasElement.height = window.innerHeight;
433
  const canvasCtx = canvasElement.getContext('2d');
434
-
 
435
  const sizeSlider = document.getElementById('size-slider');
436
  const sizeValue = document.getElementById('size-value');
437
  const lengthSlider = document.getElementById('length-slider');
@@ -443,90 +488,44 @@
443
  const flipButton = document.querySelector('.camera-flip');
444
  const snapButton = document.querySelector('.snap-button');
445
  const designOptions = document.querySelectorAll('.design-option');
446
-
447
- // Setup event listeners
448
- sizeSlider.addEventListener('input', (e) => {
449
- sizeScale = parseInt(e.target.value) / 100;
450
- sizeValue.textContent = `${e.target.value}%`;
451
- });
452
-
453
- lengthSlider.addEventListener('input', (e) => {
454
- lengthScale = parseInt(e.target.value) / 100;
455
- lengthValue.textContent = `${e.target.value}%`;
456
- });
457
-
458
- opacitySlider.addEventListener('input', (e) => {
459
- opacity = parseInt(e.target.value) / 100;
460
- opacityValue.textContent = `${e.target.value}%`;
461
- });
462
-
463
- shadowToggle.addEventListener('click', () => {
464
- shadowToggle.classList.toggle('active');
465
- showShadows = shadowToggle.classList.contains('active');
466
- });
467
-
468
- linesToggle.addEventListener('click', () => {
469
- linesToggle.classList.toggle('active');
470
- showLines = linesToggle.classList.contains('active');
471
- });
472
-
473
- flipButton.addEventListener('click', () => {
474
- facingMode = facingMode === 'user' ? 'environment' : 'user';
475
- // Restart camera with new facingMode
476
- camera.stop();
477
- setupCamera();
478
- });
479
-
480
- designOptions.forEach(option => {
481
- option.addEventListener('click', () => {
482
- designOptions.forEach(opt => opt.classList.remove('selected'));
483
- option.classList.add('selected');
484
- currentDesign = option.getAttribute('data-design');
485
- });
486
- });
487
-
488
- snapButton.addEventListener('click', () => {
489
- const dataUrl = canvasElement.toDataURL('image/png');
490
-
491
- // Create a temporary link element
492
- const link = document.createElement('a');
493
- link.href = dataUrl;
494
- link.download = 'AR-Nail-Design.png';
495
- document.body.appendChild(link);
496
- link.click();
497
- document.body.removeChild(link);
498
-
499
- // Visual feedback for snapshot
500
- snapButton.style.backgroundColor = '#3a86ff';
501
  setTimeout(() => {
502
- snapButton.style.backgroundColor = 'white';
503
- }, 300);
504
  });
505
-
506
- // Function to apply nail design to a finger tip
 
 
 
 
 
507
  function applyNailDesign(ctx, fingerTip, fingerBase, design) {
508
- // Extract coordinates
509
  const tipX = fingerTip.x * canvasElement.width;
510
  const tipY = fingerTip.y * canvasElement.height;
511
  const baseX = fingerBase.x * canvasElement.width;
512
  const baseY = fingerBase.y * canvasElement.height;
513
 
514
- // Calculate nail parameters
515
  const fingerAngle = Math.atan2(tipY - baseY, tipX - baseX);
516
  const fingerLength = Math.sqrt(Math.pow(tipX - baseX, 2) + Math.pow(tipY - baseY, 2));
517
 
518
- // Size of the nail (adjusted by slider)
519
  const nailWidth = fingerLength * 0.6 * sizeScale;
520
  const nailLength = fingerLength * 0.7 * lengthScale;
521
 
522
- // Save context state
523
  ctx.save();
524
 
525
- // Move to the finger tip and rotate
526
  ctx.translate(tipX, tipY);
527
  ctx.rotate(fingerAngle);
528
 
529
- // Draw nail shadow if enabled
530
  if (showShadows) {
531
  ctx.save();
532
  ctx.shadowColor = nailDesigns[design].shadowColor;
@@ -540,15 +539,15 @@
540
  ctx.restore();
541
  }
542
 
543
- // Draw nail shape (ellipse)
544
  ctx.beginPath();
545
  ctx.ellipse(0, 0, nailWidth/2, nailLength/2, 0, 0, Math.PI * 2);
546
  ctx.clip();
547
 
548
- // Create gradient for the nail color
549
  const gradient = ctx.createLinearGradient(-nailWidth/2, -nailLength/2, nailWidth/2, nailLength/2);
550
 
551
- // Parse gradient colors from the design
552
  const gradientStr = nailDesigns[design].color;
553
  const colorStart = gradientStr.substring(
554
  gradientStr.indexOf('(') + 1,
@@ -562,80 +561,185 @@
562
  gradient.addColorStop(0, colorStart);
563
  gradient.addColorStop(1, colorEnd);
564
 
565
- // Draw the nail design with gradient
566
  ctx.globalAlpha = opacity;
567
  ctx.fillStyle = gradient;
568
  ctx.fillRect(-nailWidth/2, -nailLength/2, nailWidth, nailLength);
569
 
570
- // Restore context
571
  ctx.restore();
572
  }
573
-
574
- // MediaPipe Hands model callback
575
  function onResults(results) {
576
- // Clear canvas and draw camera feed
577
  canvasCtx.save();
578
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
579
  canvasCtx.drawImage(
580
  results.image, 0, 0, canvasElement.width, canvasElement.height
581
  );
582
 
583
- // Process hands
584
  if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
585
  for (const landmarks of results.multiHandLandmarks) {
586
- // Draw hand skeleton if enabled
587
  if (showLines) {
588
  drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS,
589
  {color: 'rgba(255, 255, 255, 0.5)', lineWidth: 2});
590
  }
591
 
592
- // Apply nail designs to each fingertip
593
- // Thumb
594
  applyNailDesign(canvasCtx, landmarks[4], landmarks[3], currentDesign);
595
- // Index finger
596
  applyNailDesign(canvasCtx, landmarks[8], landmarks[7], currentDesign);
597
- // Middle finger
598
  applyNailDesign(canvasCtx, landmarks[12], landmarks[11], currentDesign);
599
- // Ring finger
600
  applyNailDesign(canvasCtx, landmarks[16], landmarks[15], currentDesign);
601
- // Pinky
602
  applyNailDesign(canvasCtx, landmarks[20], landmarks[19], currentDesign);
603
  }
604
  }
605
 
606
  canvasCtx.restore();
607
  }
608
-
609
- // Initialize MediaPipe Hands
610
- const hands = new Hands({locateFile: (file) => {
611
- return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
612
- }});
613
- hands.setOptions({
614
- maxNumHands: 2,
615
- modelComplexity: 1,
616
- minDetectionConfidence: 0.5,
617
- minTrackingConfidence: 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  });
619
- hands.onResults(onResults);
620
-
621
- // Set up camera with appropriate facingMode
622
- function setupCamera() {
623
- const camera = new Camera(videoElement, {
624
- onFrame: async () => {
625
- await hands.send({image: videoElement});
626
- },
627
- width: 1280,
628
- height: 720,
629
- facingMode: facingMode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  });
631
- camera.start();
632
- return camera;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  }
634
-
635
- // Initialize camera
636
- let camera = setupCamera();
637
-
638
- // Handle window resize
639
  window.addEventListener('resize', () => {
640
  canvasElement.width = window.innerWidth;
641
  canvasElement.height = window.innerHeight;
 
1
  <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>AR美甲工作室</title>
7
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
 
15
  box-sizing: border-box;
16
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
17
  }
 
18
  body {
19
  background-color: #f8f8f8;
20
  overflow: hidden;
 
23
  height: 100%;
24
  touch-action: none;
25
  }
 
26
  .container {
27
  position: relative;
28
  width: 100%;
29
  height: 100%;
30
  overflow: hidden;
31
  }
 
32
  .input_video {
33
+ position: absolute;
34
+ width: 100%;
35
+ height: 100%;
36
+ left: 0;
37
+ top: 0;
38
+ object-fit: cover;
39
+ opacity: 0;
40
+ pointer-events: none;
41
  }
 
42
  .output_canvas {
43
  position: absolute;
44
  width: 100%;
 
47
  top: 0;
48
  object-fit: cover;
49
  }
 
50
  .controls-container {
51
  position: absolute;
52
  bottom: 0;
 
56
  background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
57
  z-index: 10;
58
  }
 
59
  .controls-content {
60
  max-width: 500px;
61
  margin: 0 auto;
62
  }
 
63
  .control-section {
64
  margin-bottom: 25px;
65
  }
 
66
  .section-title {
67
  color: white;
68
  font-size: 14px;
 
71
  letter-spacing: 0.5px;
72
  text-transform: uppercase;
73
  }
 
74
  .design-options {
75
  display: flex;
76
  overflow-x: auto;
 
78
  gap: 12px;
79
  scrollbar-width: none;
80
  }
 
81
  .design-options::-webkit-scrollbar {
82
  display: none;
83
  }
 
84
  .design-option {
85
  width: 60px;
86
  height: 60px;
 
91
  transition: all 0.2s ease;
92
  flex-shrink: 0;
93
  }
 
94
  .design-option.selected {
95
  border-color: #fff;
96
  transform: scale(1.05);
97
  box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
98
  }
 
99
  .slider-container {
100
  margin: 15px 0;
101
  color: white;
102
  }
 
103
  .slider-label {
104
  display: flex;
105
  justify-content: space-between;
106
  font-size: 12px;
107
  margin-bottom: 8px;
108
  }
 
109
  .slider {
110
  -webkit-appearance: none;
111
  appearance: none;
 
115
  border-radius: 2px;
116
  outline: none;
117
  }
 
118
  .slider::-webkit-slider-thumb {
119
  -webkit-appearance: none;
120
  appearance: none;
 
125
  cursor: pointer;
126
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
127
  }
 
128
  .toggle-section {
129
  display: flex;
130
  justify-content: space-between;
131
  align-items: center;
132
  }
 
133
  .toggle-container {
134
  display: flex;
135
  align-items: center;
136
  }
 
137
  .toggle-label {
138
  color: white;
139
  font-size: 14px;
140
  margin-right: 10px;
141
  }
 
142
  .toggle {
143
  position: relative;
144
  width: 46px;
 
147
  background-color: rgba(255, 255, 255, 0.3);
148
  transition: all 0.3s ease;
149
  }
 
150
  .toggle.active {
151
  background-color: #3a86ff;
152
  }
 
153
  .toggle-handle {
154
  position: absolute;
155
  top: 2px;
 
161
  transition: all 0.3s ease;
162
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
163
  }
 
164
  .toggle.active .toggle-handle {
165
  left: 24px;
166
  }
 
167
  .snap-button {
168
  position: absolute;
169
  bottom: 240px;
 
181
  border: none;
182
  outline: none;
183
  }
 
184
  .snap-button:active {
185
  transform: translateX(-50%) scale(0.95);
186
  }
 
187
  .snap-button::after {
188
  content: '';
189
  width: 52px;
 
191
  border-radius: 50%;
192
  border: 2px solid #f8f8f8;
193
  }
 
194
  .camera-flip {
195
  position: absolute;
196
  top: 20px;
 
207
  z-index: 20;
208
  font-size: 18px;
209
  }
 
210
  .loading-screen {
211
  position: fixed;
212
  top: 0;
 
221
  z-index: 9999;
222
  transition: opacity 0.5s ease;
223
  }
 
224
  .loading-spinner {
225
  width: 50px;
226
  height: 50px;
 
230
  animation: spin 1s linear infinite;
231
  margin-bottom: 20px;
232
  }
 
233
  .loading-text {
234
  color: white;
235
  font-size: 18px;
236
  font-weight: 500;
237
  }
 
238
  @keyframes spin {
239
  to {
240
  transform: rotate(360deg);
241
  }
242
  }
 
243
  .app-title {
244
  position: absolute;
245
  top: 20px;
 
255
  width: 100%;
256
  height: 100%;
257
  }
258
+
259
+ .camera-permission {
260
+ position: fixed;
261
+ top: 0;
262
+ left: 0;
263
+ width: 100%;
264
+ height: 100%;
265
+ background-color: rgba(0, 0, 0, 0.8);
266
+ display: flex;
267
+ flex-direction: column;
268
+ justify-content: center;
269
+ align-items: center;
270
+ z-index: 9998;
271
+ color: white;
272
+ text-align: center;
273
+ padding: 20px;
274
+ }
275
+
276
+ .camera-permission h2 {
277
+ margin-bottom: 20px;
278
+ font-size: 24px;
279
+ }
280
+
281
+ .camera-permission p {
282
+ margin-bottom: 30px;
283
+ font-size: 16px;
284
+ max-width: 500px;
285
+ }
286
+
287
+ .allow-camera-btn {
288
+ background-color: #3a86ff;
289
+ color: white;
290
+ border: none;
291
+ padding: 12px 24px;
292
+ border-radius: 24px;
293
+ font-size: 16px;
294
+ font-weight: 500;
295
+ cursor: pointer;
296
+ }
297
+
298
+ .debug-info {
299
+ position: fixed;
300
+ top: 70px;
301
+ left: 20px;
302
+ color: white;
303
+ background-color: rgba(0,0,0,0.7);
304
+ padding: 10px;
305
+ border-radius: 5px;
306
+ font-size: 12px;
307
+ z-index: 100;
308
+ max-width: 300px;
309
+ display: none;
310
+ }
311
  </style>
312
  </head>
313
 
314
  <body>
315
  <div class="loading-screen">
316
  <div class="loading-spinner"></div>
317
+ <div class="loading-text">正在加载AR美甲工作室...</div>
318
+ </div>
319
+
320
+ <div class="camera-permission" id="cameraPermission">
321
+ <h2>需要摄像头权限</h2>
322
+ <p>AR美甲工作室需要使用您的摄像头来实时显示美甲效果。请点击下方按钮允许使用摄像头。</p>
323
+ <button class="allow-camera-btn" id="allowCameraBtn">允许使用摄像头</button>
324
  </div>
325
 
326
+ <div class="debug-info" id="debugInfo"></div>
327
+
328
  <div class="container">
329
  <video class="input_video" playsinline></video>
330
  <canvas class="output_canvas"></canvas>
331
 
332
+ <div class="app-title">AR美甲工作室</div>
333
 
334
  <button class="camera-flip">
335
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
345
  <div class="controls-container">
346
  <div class="controls-content">
347
  <div class="control-section">
348
+ <div class="section-title">美甲设计</div>
349
  <div class="design-options">
350
  <div class="design-option selected" data-design="pink">
351
  <div class="color-block" style="background: linear-gradient(45deg, #ff9a9e, #fad0c4);"></div>
 
369
  </div>
370
 
371
  <div class="control-section">
372
+ <div class="section-title">调整</div>
373
 
374
  <div class="slider-container">
375
  <div class="slider-label">
376
+ <span>大小</span>
377
  <span id="size-value">100%</span>
378
  </div>
379
  <input type="range" min="50" max="150" value="100" class="slider" id="size-slider">
 
381
 
382
  <div class="slider-container">
383
  <div class="slider-label">
384
+ <span>长度</span>
385
  <span id="length-value">100%</span>
386
  </div>
387
  <input type="range" min="80" max="200" value="100" class="slider" id="length-slider">
 
389
 
390
  <div class="slider-container">
391
  <div class="slider-label">
392
+ <span>透明度</span>
393
  <span id="opacity-value">100%</span>
394
  </div>
395
  <input type="range" min="30" max="100" value="100" class="slider" id="opacity-slider">
 
398
 
399
  <div class="control-section toggle-section">
400
  <div class="toggle-container">
401
+ <span class="toggle-label">真实阴影</span>
402
  <div class="toggle active" id="shadow-toggle">
403
  <div class="toggle-handle"></div>
404
  </div>
405
  </div>
406
 
407
  <div class="toggle-container">
408
+ <span class="toggle-label">显示手部轮廓</span>
409
  <div class="toggle" id="lines-toggle">
410
  <div class="toggle-handle"></div>
411
  </div>
 
416
  </div>
417
 
418
  <script>
419
+ // 定义美甲颜色及其属性
420
  const nailDesigns = {
421
  pink: {
422
  color: 'linear-gradient(45deg, #ff9a9e, #fad0c4)',
 
443
  shadowColor: 'rgba(35, 37, 38, 0.5)'
444
  }
445
  };
446
+
447
+ // 初始化变量
 
 
 
 
 
 
 
 
448
  let currentDesign = 'pink';
449
  let sizeScale = 1.0;
450
  let lengthScale = 1.0;
451
+ let opacity = 1.0;
452
  let showShadows = true;
453
  let showLines = false;
454
+ let facingMode = 'environment'; // 'user' = 前置摄像头, 'environment' = 后置摄像头
455
+ let camera;
456
+ let cameraInitialized = false;
457
+
458
+ // 初始化MediaPipe Hands实例
459
+ let hands;
460
+
461
+ // 调试工具
462
+ const debugInfo = document.getElementById('debugInfo');
463
+ function showDebug(text) {
464
+ debugInfo.style.display = 'block';
465
+ debugInfo.textContent = text;
466
+ }
467
+
468
+ // 获取UI元素
469
  const videoElement = document.getElementsByClassName('input_video')[0];
470
  const canvasElement = document.getElementsByClassName('output_canvas')[0];
471
+ const cameraPermissionElement = document.getElementById('cameraPermission');
472
+ const allowCameraButton = document.getElementById('allowCameraBtn');
473
+
474
+ // 设置画布尺寸
475
  canvasElement.width = window.innerWidth;
476
  canvasElement.height = window.innerHeight;
477
  const canvasCtx = canvasElement.getContext('2d');
478
+
479
+ // 获取其他UI元素
480
  const sizeSlider = document.getElementById('size-slider');
481
  const sizeValue = document.getElementById('size-value');
482
  const lengthSlider = document.getElementById('length-slider');
 
488
  const flipButton = document.querySelector('.camera-flip');
489
  const snapButton = document.querySelector('.snap-button');
490
  const designOptions = document.querySelectorAll('.design-option');
491
+
492
+ // 隐藏加载屏幕当页面准备就绪
493
+ document.addEventListener('DOMContentLoaded', function() {
494
+ document.querySelector('.loading-screen').style.opacity = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  setTimeout(() => {
496
+ document.querySelector('.loading-screen').style.display = 'none';
497
+ }, 500);
498
  });
499
+
500
+ // 摄像头权限按钮点击事件
501
+ allowCameraButton.addEventListener('click', () => {
502
+ initializeARApp();
503
+ });
504
+
505
+ // 为指尖应用美甲设计的函数
506
  function applyNailDesign(ctx, fingerTip, fingerBase, design) {
507
+ // 提取坐标
508
  const tipX = fingerTip.x * canvasElement.width;
509
  const tipY = fingerTip.y * canvasElement.height;
510
  const baseX = fingerBase.x * canvasElement.width;
511
  const baseY = fingerBase.y * canvasElement.height;
512
 
513
+ // 计算指甲参数
514
  const fingerAngle = Math.atan2(tipY - baseY, tipX - baseX);
515
  const fingerLength = Math.sqrt(Math.pow(tipX - baseX, 2) + Math.pow(tipY - baseY, 2));
516
 
517
+ // 指甲的大小(通过滑块调整)
518
  const nailWidth = fingerLength * 0.6 * sizeScale;
519
  const nailLength = fingerLength * 0.7 * lengthScale;
520
 
521
+ // 保存上下文状态
522
  ctx.save();
523
 
524
+ // 移动到指尖并旋转
525
  ctx.translate(tipX, tipY);
526
  ctx.rotate(fingerAngle);
527
 
528
+ // 如果启用,绘制指甲阴影
529
  if (showShadows) {
530
  ctx.save();
531
  ctx.shadowColor = nailDesigns[design].shadowColor;
 
539
  ctx.restore();
540
  }
541
 
542
+ // 绘制指甲形状(椭圆)
543
  ctx.beginPath();
544
  ctx.ellipse(0, 0, nailWidth/2, nailLength/2, 0, 0, Math.PI * 2);
545
  ctx.clip();
546
 
547
+ // 为指甲颜色创建渐变
548
  const gradient = ctx.createLinearGradient(-nailWidth/2, -nailLength/2, nailWidth/2, nailLength/2);
549
 
550
+ // 从设计解析渐变颜色
551
  const gradientStr = nailDesigns[design].color;
552
  const colorStart = gradientStr.substring(
553
  gradientStr.indexOf('(') + 1,
 
561
  gradient.addColorStop(0, colorStart);
562
  gradient.addColorStop(1, colorEnd);
563
 
564
+ // 使用渐变绘制指甲设计
565
  ctx.globalAlpha = opacity;
566
  ctx.fillStyle = gradient;
567
  ctx.fillRect(-nailWidth/2, -nailLength/2, nailWidth, nailLength);
568
 
569
+ // 恢复上下文
570
  ctx.restore();
571
  }
572
+
573
+ // MediaPipe Hands 模型回调
574
  function onResults(results) {
575
+ // 清除画布并绘制摄像头画面
576
  canvasCtx.save();
577
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
578
  canvasCtx.drawImage(
579
  results.image, 0, 0, canvasElement.width, canvasElement.height
580
  );
581
 
582
+ // 处理手部
583
  if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
584
  for (const landmarks of results.multiHandLandmarks) {
585
+ // 如果启用,绘制手部骨架
586
  if (showLines) {
587
  drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS,
588
  {color: 'rgba(255, 255, 255, 0.5)', lineWidth: 2});
589
  }
590
 
591
+ // 为每个指尖应用美甲设计
592
+ // 拇指
593
  applyNailDesign(canvasCtx, landmarks[4], landmarks[3], currentDesign);
594
+ // 食指
595
  applyNailDesign(canvasCtx, landmarks[8], landmarks[7], currentDesign);
596
+ // 中指
597
  applyNailDesign(canvasCtx, landmarks[12], landmarks[11], currentDesign);
598
+ // 无名指
599
  applyNailDesign(canvasCtx, landmarks[16], landmarks[15], currentDesign);
600
+ // 小指
601
  applyNailDesign(canvasCtx, landmarks[20], landmarks[19], currentDesign);
602
  }
603
  }
604
 
605
  canvasCtx.restore();
606
  }
607
+
608
+ // 初始化AR应用
609
+ function initializeARApp() {
610
+ try {
611
+ cameraPermissionElement.style.display = 'none';
612
+
613
+ if (!cameraInitialized) {
614
+ showDebug("正在初始化手部跟踪...");
615
+
616
+ // 创建 MediaPipe Hands 实例
617
+ hands = new Hands({locateFile: (file) => {
618
+ return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
619
+ }});
620
+
621
+ hands.setOptions({
622
+ maxNumHands: 2,
623
+ modelComplexity: 1,
624
+ minDetectionConfidence: 0.5,
625
+ minTrackingConfidence: 0.5
626
+ });
627
+
628
+ hands.onResults(onResults);
629
+
630
+ // 设置摄像头
631
+ setupCamera();
632
+ cameraInitialized = true;
633
+
634
+ showDebug("手部跟踪已初始化");
635
+ }
636
+ } catch (error) {
637
+ showDebug("初始化失败: " + error.message);
638
+ console.error("初始化AR应用失败:", error);
639
+ }
640
+ }
641
+
642
+ // 设置事件监听器
643
+ sizeSlider.addEventListener('input', (e) => {
644
+ sizeScale = parseInt(e.target.value) / 100;
645
+ sizeValue.textContent = `${e.target.value}%`;
646
  });
647
+
648
+ lengthSlider.addEventListener('input', (e) => {
649
+ lengthScale = parseInt(e.target.value) / 100;
650
+ lengthValue.textContent = `${e.target.value}%`;
651
+ });
652
+
653
+ opacitySlider.addEventListener('input', (e) => {
654
+ opacity = parseInt(e.target.value) / 100;
655
+ opacityValue.textContent = `${e.target.value}%`;
656
+ });
657
+
658
+ shadowToggle.addEventListener('click', () => {
659
+ shadowToggle.classList.toggle('active');
660
+ showShadows = shadowToggle.classList.contains('active');
661
+ });
662
+
663
+ linesToggle.addEventListener('click', () => {
664
+ linesToggle.classList.toggle('active');
665
+ showLines = linesToggle.classList.contains('active');
666
+ });
667
+
668
+ flipButton.addEventListener('click', () => {
669
+ facingMode = facingMode === 'user' ? 'environment' : 'user';
670
+ // 使用新的facingMode重启摄像头
671
+ if (camera) {
672
+ camera.stop();
673
+ }
674
+ setupCamera();
675
+ });
676
+
677
+ designOptions.forEach(option => {
678
+ option.addEventListener('click', () => {
679
+ designOptions.forEach(opt => opt.classList.remove('selected'));
680
+ option.classList.add('selected');
681
+ currentDesign = option.getAttribute('data-design');
682
  });
683
+ });
684
+
685
+ snapButton.addEventListener('click', () => {
686
+ const dataUrl = canvasElement.toDataURL('image/png');
687
+
688
+ // 创建临时链接元素
689
+ const link = document.createElement('a');
690
+ link.href = dataUrl;
691
+ link.download = 'AR-美甲设计.png';
692
+ document.body.appendChild(link);
693
+ link.click();
694
+ document.body.removeChild(link);
695
+
696
+ // 拍照的视觉反馈
697
+ snapButton.style.backgroundColor = '#3a86ff';
698
+ setTimeout(() => {
699
+ snapButton.style.backgroundColor = 'white';
700
+ }, 300);
701
+ });
702
+
703
+ // 设置带有适当facingMode的摄像头
704
+ function setupCamera() {
705
+ try {
706
+ showDebug("正在初始化摄像头...");
707
+
708
+ camera = new Camera(videoElement, {
709
+ onFrame: async () => {
710
+ if (hands) {
711
+ try {
712
+ await hands.send({image: videoElement});
713
+ } catch (error) {
714
+ showDebug("手部跟踪出错: " + error.message);
715
+ }
716
+ }
717
+ },
718
+ width: 1280,
719
+ height: 720,
720
+ facingMode: facingMode
721
+ });
722
+
723
+ camera.start()
724
+ .then(() => {
725
+ showDebug("摄像头已启动");
726
+ setTimeout(() => {
727
+ debugInfo.style.display = 'none';
728
+ }, 3000);
729
+ })
730
+ .catch(error => {
731
+ console.error('摄像头启动失败: ', error);
732
+ showDebug("摄像头启动失败: " + error.message);
733
+ // 显示摄像头权限界面
734
+ cameraPermissionElement.style.display = 'flex';
735
+ });
736
+ } catch (error) {
737
+ showDebug("摄像头初始化失败: " + error.message);
738
+ console.error("摄像头初始化失败:", error);
739
+ }
740
  }
741
+
742
+ // 处理窗口调整大小
 
 
 
743
  window.addEventListener('resize', () => {
744
  canvasElement.width = window.innerWidth;
745
  canvasElement.height = window.innerHeight;