samlax12 commited on
Commit
bc9d8e1
·
verified ·
1 Parent(s): 556b01d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +299 -467
index.html CHANGED
@@ -5,8 +5,11 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>AR Nail Polish Simulator</title>
7
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
8
- <script src="https://cdnjs.cloudflare.com/ajax/libs/tracking.js/1.1.3/tracking-min.js"></script>
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
 
 
 
 
10
  <style>
11
  * {
12
  margin: 0;
@@ -495,6 +498,27 @@
495
  font-weight: 500;
496
  color: #333;
497
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  </style>
499
  </head>
500
  <body>
@@ -507,6 +531,7 @@
507
  <div class="video-container">
508
  <video id="video" autoplay playsinline></video>
509
  <canvas id="nail-overlay"></canvas>
 
510
  <div class="camera-switch">
511
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
512
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
@@ -516,6 +541,11 @@
516
  </div>
517
 
518
  <div class="controls">
 
 
 
 
 
519
  <div class="tabs">
520
  <div class="tab active" data-tab="color">Colors</div>
521
  <div class="tab" data-tab="texture">Textures</div>
@@ -703,20 +733,58 @@
703
  let currentShape = 'rounded';
704
  let currentLength = 2;
705
  let currentWidth = 2;
706
- let handTracker;
707
- let isTracking = false;
708
- let fingerPositions = [];
709
  let capturedImage = null;
 
 
 
 
 
 
 
710
 
711
  // Initialize the application
712
- // Immediately upon page load
713
  document.addEventListener('DOMContentLoaded', function() {
714
- const tutorialBtn = document.querySelector('.tutorial-btn');
715
- tutorialBtn.addEventListener('click', function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  document.querySelector('.tutorial-overlay').style.display = 'none';
717
  });
718
- });
 
 
 
719
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  // Update canvas size to match video
721
  function updateCanvasSize() {
722
  const container = document.querySelector('.video-container');
@@ -725,6 +793,8 @@
725
 
726
  canvas.width = width;
727
  canvas.height = height;
 
 
728
  }
729
 
730
  // Initialize camera
@@ -737,81 +807,133 @@
737
  }
738
  };
739
 
740
- navigator.mediaDevices.getUserMedia(constraints)
741
- .then(function(stream) {
742
- video.srcObject = stream;
 
 
 
 
 
 
 
 
 
 
 
 
743
  document.querySelector('.loading-message').style.display = 'none';
744
-
745
- // Wait for video to be loaded
746
- video.onloadedmetadata = function() {
747
- updateCanvasSize();
748
-
749
- // Start tracking
750
- startTracking();
751
- };
752
  })
753
- .catch(function(error) {
754
- console.error('Error accessing camera:', error);
755
  document.querySelector('.loading-message').textContent = 'Error accessing camera. Please check permissions.';
756
  });
757
  }
758
 
759
- // Initialize hand tracking
760
  function initHandTracking() {
761
- handTracker = new tracking.HandTracker();
762
- handTracker.setInitialScale(4);
763
- handTracker.setStepSize(2);
764
- handTracker.setEdgesDensity(0.1);
 
 
 
 
 
 
 
 
 
 
765
  }
766
 
767
- // Start tracking
768
- function startTracking() {
769
- if (!isTracking) {
770
- tracking.track('#video', handTracker);
771
- isTracking = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
 
773
- // Setup tracking event
774
- handTracker.on('track', function(event) {
775
- if (event.data.length > 0) {
776
- fingerPositions = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
 
778
- event.data.forEach(function(hand) {
779
- // For simplicity, we'll use the center of the detected hand as reference
780
- // In a real app, you'd use machine learning for actual finger detection
781
- const centerX = hand.x + hand.width / 2;
782
- const centerY = hand.y + hand.height / 2;
783
-
784
- // Simulate 5 fingers based on hand position
785
- // This is a simplified approach - a real app would use more advanced ML models
786
- for (let i = 0; i < 5; i++) {
787
- const angle = (Math.PI / 4) * i - Math.PI / 8;
788
- const distance = hand.width * 0.4;
789
-
790
- const fingerX = centerX + Math.cos(angle) * distance;
791
- const fingerY = centerY + Math.sin(angle) * distance;
792
-
793
- fingerPositions.push({
794
- x: fingerX,
795
- y: fingerY,
796
- width: hand.width * 0.15,
797
- height: hand.width * 0.3 * (currentLength / 2)
798
- });
799
- }
800
  });
801
  }
802
-
803
- // Render nails
804
- renderNails();
805
- });
806
-
807
- // Render function for smoother appearance
808
- function renderLoop() {
809
- renderNails();
810
- requestAnimationFrame(renderLoop);
811
  }
812
-
813
- renderLoop();
814
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  }
816
 
817
  // Render nails on canvas
@@ -821,44 +943,48 @@
821
  // For each detected finger position
822
  fingerPositions.forEach(function(finger) {
823
  // Draw nail based on shape
824
- drawNail(finger.x, finger.y, finger.width * (currentWidth / 2), finger.height);
825
  });
826
  }
827
 
828
  // Draw a nail at the specified position
829
- function drawNail(x, y, width, height) {
830
  ctx.save();
831
 
 
 
 
 
832
  // Set base color with opacity
833
  ctx.fillStyle = hexToRgba(currentColor, currentOpacity);
834
 
835
  // Apply different shapes
836
  switch(currentShape) {
837
  case 'rounded':
838
- drawRoundedNail(x, y, width, height);
839
  break;
840
  case 'oval':
841
- drawOvalNail(x, y, width, height);
842
  break;
843
  case 'pointy':
844
- drawPointyNail(x, y, width, height);
845
  break;
846
  case 'square':
847
- drawSquareNail(x, y, width, height);
848
  break;
849
  case 'almond':
850
- drawAlmondNail(x, y, width, height);
851
  break;
852
  case 'coffin':
853
- drawCoffinNail(x, y, width, height);
854
  break;
855
  default:
856
- drawRoundedNail(x, y, width, height);
857
  }
858
 
859
  // Apply texture if selected
860
  if (currentTexture) {
861
- applyTexture(x, y, width, height);
862
  }
863
 
864
  ctx.restore();
@@ -1191,20 +1317,43 @@
1191
  });
1192
  });
1193
 
1194
- // Camera switch
1195
  document.querySelector('.camera-switch').addEventListener('click', function() {
1196
  cameraFacingMode = cameraFacingMode === 'user' ? 'environment' : 'user';
1197
 
1198
- // Stop current video stream
1199
- if (video.srcObject) {
1200
- const tracks = video.srcObject.getTracks();
1201
- tracks.forEach(track => track.stop());
1202
  }
1203
 
1204
- // Reinitialize camera
1205
  document.querySelector('.loading-message').style.display = 'block';
1206
  document.querySelector('.loading-message').textContent = 'Switching camera...';
1207
- initCamera();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1208
  });
1209
 
1210
  // Reset button
@@ -1226,8 +1375,6 @@
1226
  document.querySelectorAll('.shape-option').forEach(el => el.classList.remove('selected'));
1227
  document.querySelector('.shape-option[data-shape="rounded"]').classList.add('selected');
1228
 
1229
- document.querySelectorAll('.preset-option').forEach(el => el.classList.remove('selected'));
1230
-
1231
  document.getElementById('opacity-slider').value = 100;
1232
  document.getElementById('opacity-value').textContent = '100%';
1233
 
@@ -1239,424 +1386,109 @@
1239
 
1240
  document.getElementById('width-slider').value = 2;
1241
  document.getElementById('width-value').textContent = 'Medium';
 
 
 
 
 
 
1242
  });
1243
 
1244
- // Capture button
1245
- document.getElementById('capture-btn').addEventListener('click', function() {
1246
- captureImage();
1247
- });
1248
-
1249
- // Share buttons
1250
- document.getElementById('share-instagram').addEventListener('click', function() {
1251
- alert('Sharing to Instagram would open Instagram app here');
1252
  document.querySelector('.share-modal').style.display = 'none';
1253
  });
1254
 
1255
- document.getElementById('share-facebook').addEventListener('click', function() {
1256
- alert('Sharing to Facebook would open Facebook app here');
1257
- document.querySelector('.share-modal').style.display = 'none';
 
 
 
 
 
1258
  });
1259
 
1260
- document.getElementById('share-download').addEventListener('click', function() {
1261
- // Download the image
1262
- const link = document.createElement('a');
1263
- link.download = 'my-nail-design.png';
1264
- link.href = document.getElementById('share-image').src;
1265
- link.click();
1266
  });
1267
 
1268
- // Close share modal
1269
- document.querySelector('.close-share').addEventListener('click', function() {
1270
- document.querySelector('.share-modal').style.display = 'none';
1271
  });
1272
  }
1273
 
1274
- // Apply preset
1275
  function applyPreset(preset) {
1276
  switch(preset) {
1277
  case 'french':
1278
- currentColor = '#FFFFFF';
 
 
1279
  currentTexture = null;
1280
- currentShape = 'rounded';
1281
- // A real app would apply a French tip design
 
 
1282
  break;
 
1283
  case 'ombre':
1284
- currentColor = '#ff80ab';
1285
- currentOpacity = 0.7;
 
1286
  currentTexture = null;
1287
- currentShape = 'almond';
1288
- // A real app would apply a gradient
1289
  break;
 
1290
  case 'glitter':
1291
- currentColor = '#f5f5f5';
 
1292
  currentTexture = 'polka';
1293
- textureOpacity = 0.8;
1294
- currentShape = 'rounded';
 
1295
  break;
 
1296
  case 'marble':
1297
- currentColor = '#ffffff';
1298
- currentTexture = 'stripes';
 
1299
  textureOpacity = 0.3;
1300
- currentShape = 'square';
 
1301
  break;
 
1302
  case 'metallic':
1303
- currentColor = '#C0C0C0';
1304
- currentOpacity = 1.0;
 
1305
  currentTexture = null;
1306
- currentShape = 'oval';
1307
  break;
 
1308
  case 'galaxy':
1309
- currentColor = '#000080';
 
1310
  currentTexture = 'stars';
1311
- textureOpacity = 0.7;
1312
- currentShape = 'pointy';
 
1313
  break;
1314
- default:
1315
- // No preset
1316
  }
1317
 
1318
- // Update UI to match preset
1319
- document.querySelectorAll('.color-option').forEach(function(option) {
1320
- if (option.getAttribute('data-color') === currentColor) {
1321
- document.querySelectorAll('.color-option').forEach(el => el.classList.remove('selected'));
1322
- option.classList.add('selected');
1323
- }
1324
- });
1325
-
1326
- document.querySelectorAll('.texture-option').forEach(function(option) {
1327
- if (option.getAttribute('data-texture') === currentTexture) {
1328
- document.querySelectorAll('.texture-option').forEach(el => el.classList.remove('selected'));
1329
- option.classList.add('selected');
1330
- }
1331
- });
1332
-
1333
- document.querySelectorAll('.shape-option').forEach(function(option) {
1334
- if (option.getAttribute('data-shape') === currentShape) {
1335
- document.querySelectorAll('.shape-option').forEach(el => el.classList.remove('selected'));
1336
- option.classList.add('selected');
1337
- }
1338
- });
1339
-
1340
- // Update sliders
1341
- document.getElementById('opacity-slider').value = currentOpacity * 100;
1342
- document.getElementById('opacity-value').textContent = Math.round(currentOpacity * 100) + '%';
1343
-
1344
- document.getElementById('texture-opacity-slider').value = textureOpacity * 100;
1345
- document.getElementById('texture-opacity-value').textContent = Math.round(textureOpacity * 100) + '%';
1346
- }
1347
-
1348
- // Capture current view
1349
- function captureImage() {
1350
- // Create a canvas that includes both video and overlay
1351
- const captureCanvas = document.createElement('canvas');
1352
- const captureCtx = captureCanvas.getContext('2d');
1353
-
1354
- captureCanvas.width = video.videoWidth;
1355
- captureCanvas.height = video.videoHeight;
1356
-
1357
- // Draw video
1358
- captureCtx.drawImage(video, 0, 0, captureCanvas.width, captureCanvas.height);
1359
-
1360
- // Scale the nail overlay to match the capture canvas
1361
- const scaleX = captureCanvas.width / canvas.width;
1362
- const scaleY = captureCanvas.height / canvas.height;
1363
-
1364
- captureCtx.save();
1365
- captureCtx.scale(scaleX, scaleY);
1366
-
1367
- // Redraw nails on the capture canvas
1368
- fingerPositions.forEach(function(finger) {
1369
- // Draw nail based on shape
1370
- drawNailOnContext(captureCtx, finger.x, finger.y, finger.width * (currentWidth / 2), finger.height);
1371
- });
1372
-
1373
- captureCtx.restore();
1374
-
1375
- // Convert to image
1376
- capturedImage = captureCanvas.toDataURL('image/png');
1377
 
1378
- // Show share modal
1379
- document.getElementById('share-image').src = capturedImage;
1380
- document.querySelector('.share-modal').style.display = 'flex';
1381
- }
1382
-
1383
- // Draw nail on a specific context (for capture)
1384
- function drawNailOnContext(context, x, y, width, height) {
1385
- context.save();
1386
-
1387
- // Set base color with opacity
1388
- context.fillStyle = hexToRgba(currentColor, currentOpacity);
1389
-
1390
- // Apply different shapes
1391
- switch(currentShape) {
1392
- case 'rounded':
1393
- drawRoundedNailOnContext(context, x, y, width, height);
1394
- break;
1395
- case 'oval':
1396
- drawOvalNailOnContext(context, x, y, width, height);
1397
- break;
1398
- case 'pointy':
1399
- drawPointyNailOnContext(context, x, y, width, height);
1400
- break;
1401
- case 'square':
1402
- drawSquareNailOnContext(context, x, y, width, height);
1403
- break;
1404
- case 'almond':
1405
- drawAlmondNailOnContext(context, x, y, width, height);
1406
- break;
1407
- case 'coffin':
1408
- drawCoffinNailOnContext(context, x, y, width, height);
1409
- break;
1410
- default:
1411
- drawRoundedNailOnContext(context, x, y, width, height);
1412
- }
1413
-
1414
- // Apply texture if selected
1415
  if (currentTexture) {
1416
- applyTextureOnContext(context, x, y, width, height);
1417
- }
1418
-
1419
- context.restore();
1420
- }
1421
-
1422
- // Draw rounded nail shape on a specific context
1423
- function drawRoundedNailOnContext(context, x, y, width, height) {
1424
- const radius = width * 0.5;
1425
-
1426
- context.beginPath();
1427
- context.moveTo(x - width, y);
1428
- context.lineTo(x - width, y - height + radius);
1429
- context.quadraticCurveTo(x - width, y - height, x - width + radius, y - height);
1430
- context.lineTo(x + width - radius, y - height);
1431
- context.quadraticCurveTo(x + width, y - height, x + width, y - height + radius);
1432
- context.lineTo(x + width, y);
1433
- context.closePath();
1434
- context.fill();
1435
- }
1436
-
1437
- // Draw oval nail shape on a specific context
1438
- function drawOvalNailOnContext(context, x, y, width, height) {
1439
- context.beginPath();
1440
- context.ellipse(x, y - height/2, width, height/2, 0, 0, Math.PI * 2);
1441
- context.fill();
1442
- }
1443
-
1444
- // Draw pointy nail shape on a specific context
1445
- function drawPointyNailOnContext(context, x, y, width, height) {
1446
- context.beginPath();
1447
- context.moveTo(x - width, y);
1448
- context.lineTo(x - width, y - height * 0.6);
1449
- context.lineTo(x, y - height);
1450
- context.lineTo(x + width, y - height * 0.6);
1451
- context.lineTo(x + width, y);
1452
- context.closePath();
1453
- context.fill();
1454
- }
1455
-
1456
- // Draw square nail shape on a specific context
1457
- function drawSquareNailOnContext(context, x, y, width, height) {
1458
- context.beginPath();
1459
- context.rect(x - width, y - height, width * 2, height);
1460
- context.fill();
1461
- }
1462
-
1463
- // Draw almond nail shape on a specific context
1464
- function drawAlmondNailOnContext(context, x, y, width, height) {
1465
- context.beginPath();
1466
- context.moveTo(x - width, y);
1467
- context.quadraticCurveTo(x - width * 0.8, y - height * 0.6, x, y - height);
1468
- context.quadraticCurveTo(x + width * 0.8, y - height * 0.6, x + width, y);
1469
- context.closePath();
1470
- context.fill();
1471
- }
1472
-
1473
- // Draw coffin nail shape on a specific context
1474
- function drawCoffinNailOnContext(context, x, y, width, height) {
1475
- context.beginPath();
1476
- context.moveTo(x - width, y);
1477
- context.lineTo(x - width, y - height * 0.7);
1478
- context.lineTo(x - width * 0.5, y - height);
1479
- context.lineTo(x + width * 0.5, y - height);
1480
- context.lineTo(x + width, y - height * 0.7);
1481
- context.lineTo(x + width, y);
1482
- context.closePath();
1483
- context.fill();
1484
- }
1485
-
1486
- // Apply texture to the nail on a specific context
1487
- function applyTextureOnContext(context, x, y, width, height) {
1488
- context.globalAlpha = textureOpacity;
1489
-
1490
- // Different texture patterns
1491
- switch(currentTexture) {
1492
- case 'grid':
1493
- drawGridTextureOnContext(context, x, y, width, height);
1494
- break;
1495
- case 'polka':
1496
- drawPolkaDotTextureOnContext(context, x, y, width, height);
1497
- break;
1498
- case 'stripes':
1499
- drawStripedTextureOnContext(context, x, y, width, height);
1500
- break;
1501
- case 'chevron':
1502
- drawChevronTextureOnContext(context, x, y, width, height);
1503
- break;
1504
- case 'stars':
1505
- drawStarsTextureOnContext(context, x, y, width, height);
1506
- break;
1507
- case 'hearts':
1508
- drawHeartsTextureOnContext(context, x, y, width, height);
1509
- break;
1510
- default:
1511
- // No texture
1512
  }
1513
 
1514
- context.globalAlpha = 1.0;
1515
- }
1516
-
1517
- // Draw grid texture on a specific context
1518
- function drawGridTextureOnContext(context, x, y, width, height) {
1519
- context.strokeStyle = '#000';
1520
- context.lineWidth = 1;
1521
-
1522
- const gridSize = width * 0.3;
1523
-
1524
- // Vertical lines
1525
- for (let i = -width; i <= width; i += gridSize) {
1526
- context.beginPath();
1527
- context.moveTo(x + i, y - height);
1528
- context.lineTo(x + i, y);
1529
- context.stroke();
1530
- }
1531
-
1532
- // Horizontal lines
1533
- for (let i = 0; i <= height; i += gridSize) {
1534
- context.beginPath();
1535
- context.moveTo(x - width, y - i);
1536
- context.lineTo(x + width, y - i);
1537
- context.stroke();
1538
- }
1539
- }
1540
-
1541
- // Draw polka dot texture on a specific context
1542
- function drawPolkaDotTextureOnContext(context, x, y, width, height) {
1543
- context.fillStyle = '#000';
1544
- const dotSize = width * 0.15;
1545
- const spacing = width * 0.4;
1546
-
1547
- for (let i = -width + dotSize; i < width; i += spacing) {
1548
- for (let j = -height + dotSize; j < 0; j += spacing) {
1549
- context.beginPath();
1550
- context.arc(x + i, y + j, dotSize, 0, Math.PI * 2);
1551
- context.fill();
1552
- }
1553
- }
1554
- }
1555
-
1556
- // Draw striped texture on a specific context
1557
- function drawStripedTextureOnContext(context, x, y, width, height) {
1558
- context.fillStyle = '#000';
1559
- const stripeWidth = width * 0.3;
1560
-
1561
- for (let i = -width; i < width; i += stripeWidth * 2) {
1562
- context.fillRect(x + i, y - height, stripeWidth, height);
1563
- }
1564
- }
1565
-
1566
- // Draw chevron texture on a specific context
1567
- function drawChevronTextureOnContext(context, x, y, width, height) {
1568
- context.strokeStyle = '#000';
1569
- context.lineWidth = 2;
1570
-
1571
- const chevronSize = width * 0.5;
1572
-
1573
- for (let i = 0; i <= height + chevronSize; i += chevronSize) {
1574
- context.beginPath();
1575
- context.moveTo(x - width, y - i);
1576
- context.lineTo(x, y - i - chevronSize / 2);
1577
- context.lineTo(x + width, y - i);
1578
- context.stroke();
1579
- }
1580
- }
1581
-
1582
- // Draw stars texture on a specific context
1583
- function drawStarsTextureOnContext(context, x, y, width, height) {
1584
- context.fillStyle = '#000';
1585
- const starSize = width * 0.3;
1586
- const spacing = width * 0.7;
1587
-
1588
- for (let i = -width + starSize; i < width; i += spacing) {
1589
- for (let j = -height + starSize; j < 0; j += spacing) {
1590
- drawStarOnContext(context, x + i, y + j, 5, starSize * 0.5, starSize * 0.2);
1591
- }
1592
- }
1593
- }
1594
-
1595
- // Draw a star shape on a specific context
1596
- function drawStarOnContext(context, cx, cy, spikes, outerRadius, innerRadius) {
1597
- let rot = Math.PI / 2 * 3;
1598
- let x = cx;
1599
- let y = cy;
1600
- let step = Math.PI / spikes;
1601
-
1602
- context.beginPath();
1603
- context.moveTo(cx, cy - outerRadius);
1604
-
1605
- for (let i = 0; i < spikes; i++) {
1606
- x = cx + Math.cos(rot) * outerRadius;
1607
- y = cy + Math.sin(rot) * outerRadius;
1608
- context.lineTo(x, y);
1609
- rot += step;
1610
-
1611
- x = cx + Math.cos(rot) * innerRadius;
1612
- y = cy + Math.sin(rot) * innerRadius;
1613
- context.lineTo(x, y);
1614
- rot += step;
1615
- }
1616
-
1617
- context.lineTo(cx, cy - outerRadius);
1618
- context.closePath();
1619
- context.fill();
1620
- }
1621
-
1622
- // Draw hearts texture on a specific context
1623
- function drawHeartsTextureOnContext(context, x, y, width, height) {
1624
- context.fillStyle = '#000';
1625
- const heartSize = width * 0.3;
1626
- const spacing = width * 0.7;
1627
-
1628
- for (let i = -width + heartSize; i < width; i += spacing) {
1629
- for (let j = -height + heartSize; j < 0; j += spacing) {
1630
- drawHeartOnContext(context, x + i, y + j, heartSize);
1631
- }
1632
- }
1633
- }
1634
-
1635
- // Draw a heart shape on a specific context
1636
- function drawHeartOnContext(context, x, y, size) {
1637
- context.beginPath();
1638
- context.moveTo(x, y - size * 0.3);
1639
- context.bezierCurveTo(
1640
- x, y - size * 0.7,
1641
- x - size, y - size * 0.7,
1642
- x - size, y - size * 0.3
1643
- );
1644
- context.bezierCurveTo(
1645
- x - size, y + size * 0.3,
1646
- x, y + size * 0.7,
1647
- x, y + size * 0.3
1648
- );
1649
- context.bezierCurveTo(
1650
- x, y + size * 0.7,
1651
- x + size, y + size * 0.3,
1652
- x + size, y - size * 0.3
1653
- );
1654
- context.bezierCurveTo(
1655
- x + size, y - size * 0.7,
1656
- x, y - size * 0.7,
1657
- x, y - size * 0.3
1658
- );
1659
- context.fill();
1660
  }
1661
 
1662
  // Convert hex color to rgba
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>AR Nail Polish Simulator</title>
7
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
 
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
9
+ <!-- MediaPipe dependencies -->
10
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3.1640029074/camera_utils.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1646424915/hands.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils@0.3.1620248257/drawing_utils.js"></script>
13
  <style>
14
  * {
15
  margin: 0;
 
498
  font-weight: 500;
499
  color: #333;
500
  }
501
+
502
+ .manual-mode-toggle {
503
+ display: flex;
504
+ align-items: center;
505
+ margin-bottom: 15px;
506
+ cursor: pointer;
507
+ }
508
+
509
+ .manual-mode-toggle input {
510
+ margin-right: 10px;
511
+ }
512
+
513
+ #debug-canvas {
514
+ display: none;
515
+ position: absolute;
516
+ top: 0;
517
+ left: 0;
518
+ width: 100%;
519
+ height: 100%;
520
+ pointer-events: none;
521
+ }
522
  </style>
523
  </head>
524
  <body>
 
531
  <div class="video-container">
532
  <video id="video" autoplay playsinline></video>
533
  <canvas id="nail-overlay"></canvas>
534
+ <canvas id="debug-canvas"></canvas>
535
  <div class="camera-switch">
536
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
537
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
 
541
  </div>
542
 
543
  <div class="controls">
544
+ <div class="manual-mode-toggle">
545
+ <input type="checkbox" id="manual-mode-checkbox">
546
+ <label for="manual-mode-checkbox">Manual Placement Mode</label>
547
+ </div>
548
+
549
  <div class="tabs">
550
  <div class="tab active" data-tab="color">Colors</div>
551
  <div class="tab" data-tab="texture">Textures</div>
 
733
  let currentShape = 'rounded';
734
  let currentLength = 2;
735
  let currentWidth = 2;
 
 
 
736
  let capturedImage = null;
737
+ let manualMode = false;
738
+ let fingerPositions = [];
739
+ let hands; // MediaPipe Hands model
740
+ let camera; // MediaPipe camera utility
741
+ let isProcessing = false;
742
+ let debugCanvas;
743
+ let debugCtx;
744
 
745
  // Initialize the application
 
746
  document.addEventListener('DOMContentLoaded', function() {
747
+ // Get elements
748
+ video = document.getElementById('video');
749
+ canvas = document.getElementById('nail-overlay');
750
+ ctx = canvas.getContext('2d');
751
+ debugCanvas = document.getElementById('debug-canvas');
752
+ debugCtx = debugCanvas.getContext('2d');
753
+
754
+ // Set up canvas size
755
+ updateCanvasSize();
756
+ window.addEventListener('resize', updateCanvasSize);
757
+
758
+ // Set up hand tracking
759
+ initHandTracking();
760
+
761
+ // Set up camera
762
+ initCamera();
763
+
764
+ // Set up event listeners
765
+ setupEventListeners();
766
+
767
+ // Make sure the tutorial button works
768
+ document.querySelector('.tutorial-btn').addEventListener('click', function() {
769
  document.querySelector('.tutorial-overlay').style.display = 'none';
770
  });
771
+
772
+ // Manual mode toggle
773
+ document.getElementById('manual-mode-checkbox').addEventListener('change', function() {
774
+ manualMode = this.checked;
775
 
776
+ if (manualMode) {
777
+ // Add click event to canvas for manual placement
778
+ canvas.style.pointerEvents = 'auto';
779
+ canvas.addEventListener('click', handleCanvasClick);
780
+ } else {
781
+ // Remove click event and reset pointer events
782
+ canvas.style.pointerEvents = 'none';
783
+ canvas.removeEventListener('click', handleCanvasClick);
784
+ }
785
+ });
786
+ });
787
+
788
  // Update canvas size to match video
789
  function updateCanvasSize() {
790
  const container = document.querySelector('.video-container');
 
793
 
794
  canvas.width = width;
795
  canvas.height = height;
796
+ debugCanvas.width = width;
797
+ debugCanvas.height = height;
798
  }
799
 
800
  // Initialize camera
 
807
  }
808
  };
809
 
810
+ // Set up MediaPipe camera utility
811
+ camera = new window.cameraPipe.Camera(video, {
812
+ onFrame: async () => {
813
+ if (!isProcessing) {
814
+ isProcessing = true;
815
+ await hands.send({image: video});
816
+ isProcessing = false;
817
+ }
818
+ },
819
+ width: 1280,
820
+ height: 720
821
+ });
822
+
823
+ camera.start()
824
+ .then(() => {
825
  document.querySelector('.loading-message').style.display = 'none';
826
+ updateCanvasSize();
 
 
 
 
 
 
 
827
  })
828
+ .catch((error) => {
829
+ console.error('Error starting camera:', error);
830
  document.querySelector('.loading-message').textContent = 'Error accessing camera. Please check permissions.';
831
  });
832
  }
833
 
834
+ // Initialize hand tracking with MediaPipe
835
  function initHandTracking() {
836
+ hands = new window.Hands({
837
+ locateFile: (file) => {
838
+ return `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1646424915/${file}`;
839
+ }
840
+ });
841
+
842
+ hands.setOptions({
843
+ maxNumHands: 2,
844
+ modelComplexity: 1,
845
+ minDetectionConfidence: 0.5,
846
+ minTrackingConfidence: 0.5
847
+ });
848
+
849
+ hands.onResults(onHandResults);
850
  }
851
 
852
+ // Process hand tracking results
853
+ function onHandResults(results) {
854
+ // Clear previous drawings
855
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
856
+
857
+ // Debug visualization
858
+ if (debugCanvas.style.display === 'block') {
859
+ debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
860
+ if (results.multiHandLandmarks) {
861
+ for (const landmarks of results.multiHandLandmarks) {
862
+ window.drawConnectors(debugCtx, landmarks, window.HAND_CONNECTIONS,
863
+ {color: '#00FF00', lineWidth: 2});
864
+ window.drawLandmarks(debugCtx, landmarks,
865
+ {color: '#FF0000', lineWidth: 1, radius: 3});
866
+ }
867
+ }
868
+ }
869
+
870
+ // If manual mode is active, don't update automatically
871
+ if (manualMode) {
872
+ renderNails();
873
+ return;
874
+ }
875
+
876
+ // Extract fingertip positions from hand landmarks
877
+ if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
878
+ fingerPositions = [];
879
 
880
+ for (const landmarks of results.multiHandLandmarks) {
881
+ // MediaPipe hand model has 21 landmarks, with fingertips at indices:
882
+ // Thumb: 4, Index: 8, Middle: 12, Ring: 16, Pinky: 20
883
+ const fingertipIndices = [4, 8, 12, 16, 20];
884
+
885
+ for (const index of fingertipIndices) {
886
+ // Calculate nail position and size based on hand landmarks
887
+ const tip = landmarks[index];
888
+ const joint = landmarks[index - 2]; // Base joint of the finger
889
+
890
+ // Convert normalized coordinates to canvas coordinates
891
+ const tipX = tip.x * canvas.width;
892
+ const tipY = tip.y * canvas.height;
893
+ const jointX = joint.x * canvas.width;
894
+ const jointY = joint.y * canvas.height;
895
+
896
+ // Calculate nail dimensions based on finger orientation
897
+ const dx = tipX - jointX;
898
+ const dy = tipY - jointY;
899
+ const angle = Math.atan2(dy, dx);
900
 
901
+ // Calculate distance between points for size reference
902
+ const distance = Math.sqrt(dx * dx + dy * dy);
903
+
904
+ // Add to finger positions
905
+ fingerPositions.push({
906
+ x: tipX,
907
+ y: tipY,
908
+ width: distance * 0.5 * (currentWidth / 2),
909
+ height: distance * 0.8 * (currentLength / 2),
910
+ angle: angle
 
 
 
 
 
 
 
 
 
 
 
 
911
  });
912
  }
 
 
 
 
 
 
 
 
 
913
  }
 
 
914
  }
915
+
916
+ // Render nails
917
+ renderNails();
918
+ }
919
+
920
+ // Handle click on canvas for manual placement
921
+ function handleCanvasClick(event) {
922
+ const rect = canvas.getBoundingClientRect();
923
+ const x = event.clientX - rect.left;
924
+ const y = event.clientY - rect.top;
925
+
926
+ // Add a nail at the clicked position
927
+ fingerPositions.push({
928
+ x: x,
929
+ y: y,
930
+ width: 20 * (currentWidth / 2),
931
+ height: 40 * (currentLength / 2),
932
+ angle: -Math.PI / 2 // Pointing upward
933
+ });
934
+
935
+ // Re-render the nails
936
+ renderNails();
937
  }
938
 
939
  // Render nails on canvas
 
943
  // For each detected finger position
944
  fingerPositions.forEach(function(finger) {
945
  // Draw nail based on shape
946
+ drawNail(finger.x, finger.y, finger.width, finger.height, finger.angle);
947
  });
948
  }
949
 
950
  // Draw a nail at the specified position
951
+ function drawNail(x, y, width, height, angle) {
952
  ctx.save();
953
 
954
+ // Transform context based on finger orientation
955
+ ctx.translate(x, y);
956
+ ctx.rotate(angle);
957
+
958
  // Set base color with opacity
959
  ctx.fillStyle = hexToRgba(currentColor, currentOpacity);
960
 
961
  // Apply different shapes
962
  switch(currentShape) {
963
  case 'rounded':
964
+ drawRoundedNail(0, 0, width, height);
965
  break;
966
  case 'oval':
967
+ drawOvalNail(0, 0, width, height);
968
  break;
969
  case 'pointy':
970
+ drawPointyNail(0, 0, width, height);
971
  break;
972
  case 'square':
973
+ drawSquareNail(0, 0, width, height);
974
  break;
975
  case 'almond':
976
+ drawAlmondNail(0, 0, width, height);
977
  break;
978
  case 'coffin':
979
+ drawCoffinNail(0, 0, width, height);
980
  break;
981
  default:
982
+ drawRoundedNail(0, 0, width, height);
983
  }
984
 
985
  // Apply texture if selected
986
  if (currentTexture) {
987
+ applyTexture(0, 0, width, height);
988
  }
989
 
990
  ctx.restore();
 
1317
  });
1318
  });
1319
 
1320
+ // Camera switch button
1321
  document.querySelector('.camera-switch').addEventListener('click', function() {
1322
  cameraFacingMode = cameraFacingMode === 'user' ? 'environment' : 'user';
1323
 
1324
+ // Stop current camera
1325
+ if (camera) {
1326
+ camera.stop();
 
1327
  }
1328
 
1329
+ // Show loading message
1330
  document.querySelector('.loading-message').style.display = 'block';
1331
  document.querySelector('.loading-message').textContent = 'Switching camera...';
1332
+
1333
+ // Reinitialize camera with new facing mode
1334
+ setTimeout(initCamera, 500);
1335
+ });
1336
+
1337
+ // Capture button
1338
+ document.getElementById('capture-btn').addEventListener('click', function() {
1339
+ // Create a composite image of video and overlay
1340
+ const captureCanvas = document.createElement('canvas');
1341
+ captureCanvas.width = canvas.width;
1342
+ captureCanvas.height = canvas.height;
1343
+ const captureCtx = captureCanvas.getContext('2d');
1344
+
1345
+ // Draw video frame
1346
+ captureCtx.drawImage(video, 0, 0, captureCanvas.width, captureCanvas.height);
1347
+
1348
+ // Draw nail overlay
1349
+ captureCtx.drawImage(canvas, 0, 0);
1350
+
1351
+ // Convert to image data URL
1352
+ capturedImage = captureCanvas.toDataURL('image/png');
1353
+
1354
+ // Show in share modal
1355
+ document.getElementById('share-image').src = capturedImage;
1356
+ document.querySelector('.share-modal').style.display = 'flex';
1357
  });
1358
 
1359
  // Reset button
 
1375
  document.querySelectorAll('.shape-option').forEach(el => el.classList.remove('selected'));
1376
  document.querySelector('.shape-option[data-shape="rounded"]').classList.add('selected');
1377
 
 
 
1378
  document.getElementById('opacity-slider').value = 100;
1379
  document.getElementById('opacity-value').textContent = '100%';
1380
 
 
1386
 
1387
  document.getElementById('width-slider').value = 2;
1388
  document.getElementById('width-value').textContent = 'Medium';
1389
+
1390
+ // If in manual mode, clear finger positions
1391
+ if (manualMode) {
1392
+ fingerPositions = [];
1393
+ renderNails();
1394
+ }
1395
  });
1396
 
1397
+ // Share modal close button
1398
+ document.querySelector('.close-share').addEventListener('click', function() {
 
 
 
 
 
 
1399
  document.querySelector('.share-modal').style.display = 'none';
1400
  });
1401
 
1402
+ // Share buttons
1403
+ document.getElementById('share-download').addEventListener('click', function() {
1404
+ if (capturedImage) {
1405
+ const link = document.createElement('a');
1406
+ link.href = capturedImage;
1407
+ link.download = 'nail-design.png';
1408
+ link.click();
1409
+ }
1410
  });
1411
 
1412
+ document.getElementById('share-instagram').addEventListener('click', function() {
1413
+ alert('This would open Instagram sharing if implemented in a production environment.');
1414
+ // In production: would use Web Share API or Instagram API
 
 
 
1415
  });
1416
 
1417
+ document.getElementById('share-facebook').addEventListener('click', function() {
1418
+ alert('This would open Facebook sharing if implemented in a production environment.');
1419
+ // In production: would use Web Share API or Facebook SDK
1420
  });
1421
  }
1422
 
1423
+ // Apply preset nail designs
1424
  function applyPreset(preset) {
1425
  switch(preset) {
1426
  case 'french':
1427
+ // French manicure - white tips on light pink base
1428
+ currentColor = '#FFF5F5';
1429
+ currentOpacity = 0.9;
1430
  currentTexture = null;
1431
+ currentShape = 'oval';
1432
+
1433
+ // We would need a more complex rendering logic to add the white tips
1434
+ // This is simplified for this demo
1435
  break;
1436
+
1437
  case 'ombre':
1438
+ // Ombre effect - gradient would require custom rendering
1439
+ currentColor = '#f48fb1';
1440
+ currentOpacity = 0.8;
1441
  currentTexture = null;
 
 
1442
  break;
1443
+
1444
  case 'glitter':
1445
+ // Glitter effect
1446
+ currentColor = '#f06292';
1447
  currentTexture = 'polka';
1448
+ textureOpacity = 0.7;
1449
+ document.getElementById('texture-opacity-slider').value = 70;
1450
+ document.getElementById('texture-opacity-value').textContent = '70%';
1451
  break;
1452
+
1453
  case 'marble':
1454
+ // Marble effect
1455
+ currentColor = '#f5f5f5';
1456
+ currentTexture = 'chevron';
1457
  textureOpacity = 0.3;
1458
+ document.getElementById('texture-opacity-slider').value = 30;
1459
+ document.getElementById('texture-opacity-value').textContent = '30%';
1460
  break;
1461
+
1462
  case 'metallic':
1463
+ // Metallic effect
1464
+ currentColor = '#FFD700';
1465
+ currentOpacity = 0.9;
1466
  currentTexture = null;
1467
+ // Would need specialized rendering for true metallic effect
1468
  break;
1469
+
1470
  case 'galaxy':
1471
+ // Galaxy effect
1472
+ currentColor = '#311B92';
1473
  currentTexture = 'stars';
1474
+ textureOpacity = 0.8;
1475
+ document.getElementById('texture-opacity-slider').value = 80;
1476
+ document.getElementById('texture-opacity-value').textContent = '80%';
1477
  break;
 
 
1478
  }
1479
 
1480
+ // Update UI to reflect preset
1481
+ document.querySelectorAll('.color-option').forEach(el => el.classList.remove('selected'));
1482
+ document.querySelectorAll('.texture-option').forEach(el => el.classList.remove('selected'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1484
  if (currentTexture) {
1485
+ const textureElement = document.querySelector(`.texture-option[data-texture="${currentTexture}"]`);
1486
+ if (textureElement) textureElement.classList.add('selected');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1487
  }
1488
 
1489
+ // Update opacity slider
1490
+ document.getElementById('opacity-slider').value = currentOpacity * 100;
1491
+ document.getElementById('opacity-value').textContent = Math.round(currentOpacity * 100) + '%';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1492
  }
1493
 
1494
  // Convert hex color to rgba