openfree commited on
Commit
02ed9d7
ยท
verified ยท
1 Parent(s): d67ad4e

Update holistic.js

Browse files
Files changed (1) hide show
  1. holistic.js +246 -192
holistic.js CHANGED
@@ -1,22 +1,24 @@
1
  import DeviceDetector from "https://cdn.skypack.dev/[email protected]";
2
- // ็”จๆณ•: testSupport({client?: string, os?: string}[])
3
- // Client ๅ’Œ os ๆ˜ฏๆญฃๅˆ™่กจ่พพๅผใ€‚
4
- // ๅ‚่ง: https://cdn.jsdelivr.net/npm/[email protected]/README.md
5
- // ไบ†่งฃ client ๅ’Œ os ็š„ๅˆๆณ•ๅ€ผ
6
- // ๅฏผๅ…ฅๅฟ…่ฆ็š„ๅบ“
7
 
8
- // ๅˆๅง‹ๅŒ–้€Ÿๅบฆๅ’ŒๅŠ ้€Ÿๅบฆๅ›พ่กจ
 
 
9
  let speedChart, accelerationChart;
10
  let previousPoseData = null;
11
  let lastTimestamp = 0;
 
12
  testSupport([
13
  { client: 'Chrome' },
14
  ]);
15
 
16
- // ๅ›พ่กจ็›ธๅ…ณ็š„ๅธธ้‡้…็ฝฎ
17
  const CHART_CONFIG = {
18
  maxDataPoints: 50,
19
- updateInterval: 100, // ๅ›พ่กจๆ›ดๆ–ฐ้—ด้š”(ms)
20
  colors: {
21
  speed: {
22
  primary: 'rgba(75, 192, 192, 1)',
@@ -29,14 +31,14 @@ const CHART_CONFIG = {
29
  }
30
  };
31
 
32
- // ๅˆๅง‹ๅŒ–ๅ›พ่กจ
33
  function initCharts() {
34
- // ๅˆ›ๅปบๅ›พ่กจๅฎนๅ™จ
35
  const chartsContainer = document.createElement('div');
36
  chartsContainer.className = 'charts-container';
37
  document.querySelector('.container').appendChild(chartsContainer);
38
 
39
- // ้€Ÿๅบฆๅ›พ่กจๅฎนๅ™จ
40
  const speedChartContainer = document.createElement('div');
41
  speedChartContainer.className = 'chart-card';
42
  const speedCanvas = document.createElement('canvas');
@@ -44,7 +46,7 @@ function initCharts() {
44
  speedChartContainer.appendChild(speedCanvas);
45
  chartsContainer.appendChild(speedChartContainer);
46
 
47
- // ๅŠ ้€Ÿๅบฆๅ›พ่กจๅฎนๅ™จ
48
  const accelerationChartContainer = document.createElement('div');
49
  accelerationChartContainer.className = 'chart-card';
50
  const accelerationCanvas = document.createElement('canvas');
@@ -52,13 +54,13 @@ function initCharts() {
52
  accelerationChartContainer.appendChild(accelerationCanvas);
53
  chartsContainer.appendChild(accelerationChartContainer);
54
 
55
- // ้€Ÿๅบฆๅ›พ่กจ้…็ฝฎ
56
  speedChart = new Chart(speedCanvas.getContext('2d'), {
57
  type: 'line',
58
  data: {
59
  labels: [],
60
  datasets: [{
61
- label: '่ฟๅŠจ้€Ÿๅบฆ (ๅƒ็ด /็ง’)',
62
  data: [],
63
  borderColor: CHART_CONFIG.colors.speed.primary,
64
  backgroundColor: CHART_CONFIG.colors.speed.background,
@@ -129,13 +131,13 @@ function initCharts() {
129
  }
130
  });
131
 
132
- // ๅŠ ้€Ÿๅบฆๅ›พ่กจ้…็ฝฎ
133
  accelerationChart = new Chart(accelerationCanvas.getContext('2d'), {
134
  type: 'line',
135
  data: {
136
  labels: [],
137
  datasets: [{
138
- label: 'ๅŠ ้€Ÿๅบฆ (ๅƒ็ด /็ง’ยฒ)',
139
  data: [],
140
  borderColor: CHART_CONFIG.colors.acceleration.primary,
141
  backgroundColor: CHART_CONFIG.colors.acceleration.background,
@@ -206,21 +208,21 @@ function initCharts() {
206
  }
207
  });
208
 
209
- // ๆทปๅŠ ๅ“ๅบ”ๅผๅค„็†
210
  window.addEventListener('resize', () => {
211
  speedChart.resize();
212
  accelerationChart.resize();
213
  });
214
  }
215
 
216
- // ่ฎก็ฎ—ๅงฟๆ€ๅ˜ๅŒ–็š„้€Ÿๅบฆๅ’ŒๅŠ ้€Ÿๅบฆ
217
  function calculateMotionMetrics(results, timestamp) {
218
- // ๅŸบๆœฌ้ชŒ่ฏ
219
  if (!results || !results.poseLandmarks || !Array.isArray(results.poseLandmarks)) {
220
  return { speed: 0, acceleration: 0 };
221
  }
222
 
223
- // ๅˆๅง‹ๅŒ–็Šถๆ€
224
  if (!previousPoseData || !previousPoseData.poseLandmarks) {
225
  previousPoseData = {
226
  poseLandmarks: [...results.poseLandmarks]
@@ -229,73 +231,75 @@ function calculateMotionMetrics(results, timestamp) {
229
  return { speed: 0, acceleration: 0 };
230
  }
231
 
232
- const deltaTime = (timestamp - lastTimestamp) / 1000; // ่ฝฌๆขไธบ็ง’
233
  if (deltaTime === 0) {
234
  return { speed: 0, acceleration: 0 };
235
  }
236
 
237
- // ่ฎก็ฎ—ๅ…ณ้”ฎ็‚น็š„ๅนณๅ‡ไฝ็งป
238
  let totalDisplacement = 0;
239
  let validPoints = 0;
240
 
241
  try {
242
- // ๅชไฝฟ็”จๆœ‰ๆ•ˆ็š„ๅ…ณ้”ฎ็‚น่ฟ›่กŒ่ฎก็ฎ—
243
  results.poseLandmarks.forEach((landmark, index) => {
244
  const prevLandmark = previousPoseData.poseLandmarks[index];
245
 
246
- // ็กฎไฟๅฝ“ๅ‰ๅ’Œๅ‰ไธ€ๅธง็š„ๅ…ณ้”ฎ็‚น้ƒฝๅญ˜ๅœจไธ”ๆœ‰ๆ•ˆ
247
- if (landmark && prevLandmark &&
248
  typeof landmark.x === 'number' &&
249
  typeof landmark.y === 'number' &&
250
  typeof prevLandmark.x === 'number' &&
251
  typeof prevLandmark.y === 'number' &&
252
- // ๅฏ้€‰๏ผšๆฃ€ๆŸฅๅฏ่งๆ€ง้˜ˆๅ€ผ
253
  (!landmark.visibility || landmark.visibility > 0.5) &&
254
- (!prevLandmark.visibility || prevLandmark.visibility > 0.5)) {
255
-
256
  const dx = landmark.x - prevLandmark.x;
257
  const dy = landmark.y - prevLandmark.y;
258
  const displacement = Math.sqrt(dx * dx + dy * dy);
259
 
260
- // ่ฟ‡ๆปคๆމๅผ‚ๅธธๅคง็š„ไฝ็งป
261
- if (displacement < 1.0) { // ๆ นๆฎ้œ€่ฆ่ฐƒๆ•ด้˜ˆๅ€ผ
262
  totalDisplacement += displacement;
263
  validPoints++;
264
  }
265
  }
266
  });
267
  } catch (error) {
268
- console.warn('Error calculating displacement:', error);
269
  return { speed: 0, acceleration: 0 };
270
  }
271
 
272
- // ๅฆ‚ๆžœๆฒกๆœ‰ๆœ‰ๆ•ˆ็‚น๏ผŒ่ฟ”ๅ›ž้›ถๅ€ผ
273
  if (validPoints === 0) {
274
  return { speed: 0, acceleration: 0 };
275
  }
276
 
277
- // ่ฎก็ฎ—ๅนณๅ‡ไฝ็งปๅ’Œ้€Ÿๅบฆ
278
  const averageDisplacement = totalDisplacement / validPoints;
279
  const currentSpeed = averageDisplacement / deltaTime;
280
 
281
- // ่Žทๅ–ไธŠไธ€ๆฌก็š„้€Ÿๅบฆ๏ผŒๅฆ‚ๆžœไธๅญ˜ๅœจๅˆ™ไฝฟ็”จ0
282
  let previousSpeed = 0;
283
  try {
284
- previousSpeed = speedChart.data.datasets[0].data[speedChart.data.datasets[0].data.length - 1] || 0;
 
 
285
  } catch (error) {
286
- console.warn('Error accessing previous speed:', error);
287
  }
288
 
289
- // ่ฎก็ฎ—ๅŠ ้€Ÿๅบฆ
290
  const acceleration = (currentSpeed - previousSpeed) / deltaTime;
291
 
292
- // ๆ›ดๆ–ฐๅ…ˆๅ‰ๆ•ฐๆฎ็”จไบŽไธ‹ไธ€ๅธง่ฎก็ฎ—
293
  previousPoseData = {
294
  poseLandmarks: [...results.poseLandmarks]
295
  };
296
  lastTimestamp = timestamp;
297
 
298
- // ๆทปๅŠ ไธ€ไบ›ๅŸบๆœฌ็š„ๆ•ฐๅ€ผ้ชŒ่ฏ
299
  const metrics = {
300
  speed: isFinite(currentSpeed) ? Math.min(Math.max(currentSpeed, 0), 1000) : 0,
301
  acceleration: isFinite(acceleration) ? Math.min(Math.max(acceleration, -1000), 1000) : 0
@@ -304,31 +308,31 @@ function calculateMotionMetrics(results, timestamp) {
304
  return metrics;
305
  }
306
 
307
- // ๆ›ดๆ–ฐๅ›พ่กจๆ•ฐๆฎ็š„ๅ‡ฝๆ•ฐไนŸ้œ€่ฆๆทปๅŠ ้”™่ฏฏๅค„็†
308
  function updateCharts(metrics) {
309
  if (!metrics || typeof metrics.speed !== 'number' || typeof metrics.acceleration !== 'number') {
310
- console.warn('Invalid metrics data:', metrics);
311
  return;
312
  }
313
 
314
  try {
315
- const timestamp = new Date().toLocaleTimeString('zh-CN', {
316
  hour: '2-digit',
317
  minute: '2-digit',
318
  second: '2-digit'
319
  });
320
 
321
- // ๆฃ€ๆŸฅๅ›พ่กจๅฏน่ฑกๆ˜ฏๅฆๅญ˜ๅœจไธ”ๆญฃ็กฎๅˆๅง‹ๅŒ–
322
  if (!speedChart || !speedChart.data || !speedChart.data.labels) {
323
- console.warn('Speed chart not properly initialized');
324
  return;
325
  }
326
  if (!accelerationChart || !accelerationChart.data || !accelerationChart.data.labels) {
327
- console.warn('Acceleration chart not properly initialized');
328
  return;
329
  }
330
 
331
- // ๆ›ดๆ–ฐ้€Ÿๅบฆๅ›พ่กจ
332
  speedChart.data.labels.push(timestamp);
333
  speedChart.data.datasets[0].data.push(metrics.speed);
334
  if (speedChart.data.labels.length > CHART_CONFIG.maxDataPoints) {
@@ -336,7 +340,7 @@ function updateCharts(metrics) {
336
  speedChart.data.datasets[0].data.shift();
337
  }
338
 
339
- // ๆ›ดๆ–ฐๅŠ ้€Ÿๅบฆๅ›พ่กจ
340
  accelerationChart.data.labels.push(timestamp);
341
  accelerationChart.data.datasets[0].data.push(metrics.acceleration);
342
  if (accelerationChart.data.labels.length > CHART_CONFIG.maxDataPoints) {
@@ -344,21 +348,20 @@ function updateCharts(metrics) {
344
  accelerationChart.data.datasets[0].data.shift();
345
  }
346
 
347
- // ไฝฟ็”จ requestAnimationFrame ไผ˜ๅŒ–ๅ›พ่กจๆ›ดๆ–ฐ
348
  requestAnimationFrame(() => {
349
  try {
350
  speedChart.update('none');
351
  accelerationChart.update('none');
352
  } catch (error) {
353
- console.warn('Error updating charts:', error);
354
  }
355
  });
356
  } catch (error) {
357
- console.warn('Error in updateCharts:', error);
358
  }
359
  }
360
 
361
-
362
  function testSupport(supportedDevices) {
363
  const deviceDetector = new DeviceDetector();
364
  const detectedDevice = deviceDetector.parse(navigator.userAgent);
@@ -380,8 +383,8 @@ function testSupport(supportedDevices) {
380
  break;
381
  }
382
  if (!isSupported) {
383
- alert(`ๆญคๆผ”็คบๅœจ ${detectedDevice.client.name}/${detectedDevice.os.name} ไธŠ่ฟ่กŒๆ—ถ ` +
384
- `็›ฎๅ‰ไธ่ƒฝๅพˆๅฅฝๅœฐๆ”ฏๆŒ๏ผŒ็ปง็ปญไฝฟ็”จ้œ€่‡ชๆ‹…้ฃŽ้™ฉใ€‚`);
385
  }
386
  }
387
 
@@ -393,17 +396,16 @@ const config = { locateFile: (file) => {
393
  `${mpHolistic.VERSION}/${file}`;
394
  } };
395
 
396
- // ๆˆ‘ไปฌ็š„่พ“ๅ…ฅๅธงๅฐ†ๆฅ่‡ช่ฟ™้‡Œใ€‚
397
  const videoElement = document.getElementsByClassName('input_video')[0];
398
  const canvasElement = document.getElementsByClassName('output_canvas')[0];
399
  const controlsElement = document.getElementsByClassName('control-panel')[0];
400
  const canvasCtx = canvasElement.getContext('2d');
401
 
402
- // ๆˆ‘ไปฌ็จๅŽไผšๅฐ†่ฟ™ไธชๆทปๅŠ ๅˆฐๆŽงๅˆถ้ขๆฟไธญ๏ผŒไฝ†ๆˆ‘ไปฌไผšๅœจ่ฟ™้‡Œไฟๅญ˜ๅฎƒ๏ผŒ
403
- // ไปฅไพฟๆฏๆฌกๅ›พๅฝข่ฟ่กŒๆ—ถ้ƒฝๅฏไปฅ่ฐƒ็”จ tick()ใ€‚
404
  const fpsControl = new controls.FPS();
405
 
406
- // ไผ˜ๅŒ–๏ผšๅœจ้š่—ๅŠจ็”ปๅฎŒๆˆๅŽๅ…ณ้—ญๅŠจ็”ปๆ—‹่ฝฌๅ™จใ€‚
407
  const spinner = document.querySelector('.loading');
408
  spinner.ontransitionend = () => {
409
  spinner.style.display = 'none';
@@ -417,7 +419,10 @@ function removeElements(landmarks, elements) {
417
 
418
  function removeLandmarks(results) {
419
  if (results.poseLandmarks) {
420
- removeElements(results.poseLandmarks, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 18, 19, 20, 21, 22]);
 
 
 
421
  }
422
  }
423
 
@@ -441,25 +446,27 @@ function connect(ctx, connectors) {
441
 
442
  let activeEffect = 'mask';
443
 
444
-
445
  function onResults(results) {
446
- // ้š่—ๆ—‹่ฝฌๅ™จ
447
  document.body.classList.add('loaded');
448
 
449
- // ็งป้™คๆˆ‘ไปฌไธๆƒณ็ป˜ๅˆถ็š„ๅ…ณ้”ฎ็‚น
450
  removeLandmarks(results);
451
 
452
- // ๆ›ดๆ–ฐๅธง็އ
453
  fpsControl.tick();
454
 
455
- // ็ป˜ๅˆถๅ ๅŠ ๅฑ‚
456
  canvasCtx.save();
457
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
458
 
459
  if (results.segmentationMask) {
460
- canvasCtx.drawImage(results.segmentationMask, 0, 0, canvasElement.width, canvasElement.height);
 
 
 
461
 
462
- // ไป…่ฆ†็›–็Žฐๆœ‰ๅƒ็ด 
463
  if (activeEffect === 'mask' || activeEffect === 'both') {
464
  canvasCtx.globalCompositeOperation = 'source-in';
465
  canvasCtx.fillStyle = '#00FF007F';
@@ -470,7 +477,7 @@ function onResults(results) {
470
  canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
471
  }
472
 
473
- // ไป…่ฆ†็›–็ผบๅคฑ็š„ๅƒ็ด 
474
  canvasCtx.globalCompositeOperation = 'destination-atop';
475
  canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
476
  canvasCtx.globalCompositeOperation = 'source-over';
@@ -478,17 +485,17 @@ function onResults(results) {
478
  canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
479
  }
480
 
481
- // ่ฎก็ฎ—ๅนถๆ›ดๆ–ฐๅŠจไฝœๆŒ‡ๆ ‡
482
  const metrics = calculateMotionMetrics(results, performance.now());
483
  updateCharts(metrics);
484
 
485
- // ๆทปๅŠ ๅฎ‰ๅ…จๆฃ€ๆŸฅ
486
  if (!results.poseLandmarks || !mpHolistic.POSE_LANDMARKS) {
487
  canvasCtx.restore();
488
  return;
489
  }
490
 
491
- // ่ฟžๆŽฅ่‚˜้ƒจๅˆฐๆ‰‹้ƒจ
492
  canvasCtx.lineWidth = 5;
493
  if (results.poseLandmarks) {
494
  if (results.rightHandLandmarks && results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW]) {
@@ -507,38 +514,54 @@ function onResults(results) {
507
  }
508
  }
509
 
510
- // ็ป˜ๅˆถๅงฟๆ€่ฟžๆŽฅ็‚น
511
  if (results.poseLandmarks && mpHolistic.POSE_CONNECTIONS) {
512
- drawingUtils.drawConnectors(canvasCtx, results.poseLandmarks, mpHolistic.POSE_CONNECTIONS, { color: 'white' });
 
 
 
 
 
513
  }
514
 
515
- // ็ป˜ๅˆถๅทฆไพงๅงฟๆ€ๅ…ณ้”ฎ็‚น
516
  if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_LEFT) {
517
  const leftLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_LEFT)
518
  .map(index => results.poseLandmarks[index])
519
  .filter(landmark => landmark !== undefined);
520
 
521
  if (leftLandmarks.length > 0) {
522
- drawingUtils.drawLandmarks(canvasCtx, leftLandmarks,
523
- { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(255,138,0)' });
 
 
 
524
  }
525
  }
526
 
527
- // ็ป˜ๅˆถๅณไพงๅงฟๆ€ๅ…ณ้”ฎ็‚น
528
  if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_RIGHT) {
529
  const rightLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_RIGHT)
530
  .map(index => results.poseLandmarks[index])
531
  .filter(landmark => landmark !== undefined);
532
 
533
  if (rightLandmarks.length > 0) {
534
- drawingUtils.drawLandmarks(canvasCtx, rightLandmarks,
535
- { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(0,217,231)' });
 
 
 
536
  }
537
  }
538
 
539
- // ็ป˜ๅˆถๆ‰‹้ƒจ
540
  if (results.rightHandLandmarks && mpHolistic.HAND_CONNECTIONS) {
541
- drawingUtils.drawConnectors(canvasCtx, results.rightHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' });
 
 
 
 
 
542
  drawingUtils.drawLandmarks(canvasCtx, results.rightHandLandmarks, {
543
  color: 'white',
544
  fillColor: 'rgb(0,217,231)',
@@ -550,7 +573,12 @@ function onResults(results) {
550
  }
551
 
552
  if (results.leftHandLandmarks && mpHolistic.HAND_CONNECTIONS) {
553
- drawingUtils.drawConnectors(canvasCtx, results.leftHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' });
 
 
 
 
 
554
  drawingUtils.drawLandmarks(canvasCtx, results.leftHandLandmarks, {
555
  color: 'white',
556
  fillColor: 'rgb(255,138,0)',
@@ -561,45 +589,73 @@ function onResults(results) {
561
  });
562
  }
563
 
564
- // ็ป˜ๅˆถ้ข้ƒจ
565
  if (results.faceLandmarks && mpHolistic.FACEMESH_TESSELATION) {
566
- drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_TESSELATION,
567
- { color: '#C0C0C070', lineWidth: 1 });
568
-
 
 
 
569
  if (mpHolistic.FACEMESH_RIGHT_EYE) {
570
- drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYE,
571
- { color: 'rgb(0,217,231)' });
 
 
 
 
572
  }
573
  if (mpHolistic.FACEMESH_RIGHT_EYEBROW) {
574
- drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYEBROW,
575
- { color: 'rgb(0,217,231)' });
 
 
 
 
576
  }
577
  if (mpHolistic.FACEMESH_LEFT_EYE) {
578
- drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYE,
579
- { color: 'rgb(255,138,0)' });
 
 
 
 
580
  }
581
  if (mpHolistic.FACEMESH_LEFT_EYEBROW) {
582
- drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYEBROW,
583
- { color: 'rgb(255,138,0)' });
 
 
 
 
584
  }
585
  if (mpHolistic.FACEMESH_FACE_OVAL) {
586
- drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_FACE_OVAL,
587
- { color: '#E0E0E0', lineWidth: 5 });
 
 
 
 
588
  }
589
  if (mpHolistic.FACEMESH_LIPS) {
590
- drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LIPS,
591
- { color: '#E0E0E0', lineWidth: 5 });
 
 
 
 
592
  }
593
  }
594
 
595
  canvasCtx.restore();
596
  }
597
- // ่ง†้ข‘ไธŠไผ ๅค„็†
 
598
  function handleVideoUpload(file) {
599
- // ๅˆ›ๅปบ่ง†้ข‘ URL
600
  const videoUrl = URL.createObjectURL(file);
601
 
602
- // ้‡็ฝฎๅ›พ่กจๆ•ฐๆฎ
603
  speedChart.data.labels = [];
604
  speedChart.data.datasets[0].data = [];
605
  accelerationChart.data.labels = [];
@@ -607,15 +663,15 @@ function handleVideoUpload(file) {
607
  previousPoseData = null;
608
  lastTimestamp = 0;
609
 
610
- // ้‡็ฝฎๅงฟๆ€ๆฃ€ๆต‹
611
  holistic.reset();
612
 
613
- // ๆ›ดๆ–ฐ่ง†้ข‘ๆบ
614
  videoElement.src = videoUrl;
615
 
616
- // ่ฎพ็ฝฎ่ง†้ข‘ๅŠ ่ฝฝๅฎŒๆˆๅŽ็š„ๅค„็†
617
  videoElement.onloadedmetadata = () => {
618
- // ่ฐƒๆ•ด็”ปๅธƒๅคงๅฐไปฅๅŒน้…่ง†้ข‘ๅฐบๅฏธ
619
  const aspect = videoElement.videoHeight / videoElement.videoWidth;
620
  let width, height;
621
  if (window.innerWidth > window.innerHeight) {
@@ -628,7 +684,7 @@ function handleVideoUpload(file) {
628
  canvasElement.width = width;
629
  canvasElement.height = height;
630
 
631
- // ๅˆ›ๅปบ็”จไบŽ่ง†้ข‘ๅค„็†็š„ๅŠจ็”ปๅธง
632
  let animationId;
633
 
634
  async function processFrame() {
@@ -636,24 +692,21 @@ function handleVideoUpload(file) {
636
  cancelAnimationFrame(animationId);
637
  return;
638
  }
639
-
640
- // ๅ‘้€ๅฝ“ๅ‰ๅธงๅˆฐๅงฟๆ€ๆฃ€ๆต‹
641
  await holistic.send({
642
  image: videoElement
643
  });
644
-
645
- // ็ปง็ปญๅค„็†ไธ‹ไธ€ๅธง
646
  animationId = requestAnimationFrame(processFrame);
647
  }
648
 
649
- // ่ง†้ข‘ๆ’ญๆ”พไบ‹ไปถๅค„็†
650
  videoElement.onplay = () => {
651
  processFrame();
652
  };
653
 
654
- // ๆทปๅŠ ่ง†้ข‘ๆŽงๅˆถๆŒ‰้’ฎไบ‹ไปถ็›‘ๅฌ
655
  const playPauseBtn = document.createElement('button');
656
- playPauseBtn.textContent = 'ๆ’ญๆ”พ/ๆš‚ๅœ';
657
  playPauseBtn.className = 'control-button';
658
  playPauseBtn.onclick = () => {
659
  if (videoElement.paused) {
@@ -663,8 +716,9 @@ function handleVideoUpload(file) {
663
  }
664
  };
665
 
 
666
  const restartBtn = document.createElement('button');
667
- restartBtn.textContent = '้‡ๆ–ฐๅผ€ๅง‹';
668
  restartBtn.className = 'control-button';
669
  restartBtn.onclick = () => {
670
  videoElement.currentTime = 0;
@@ -673,25 +727,25 @@ function handleVideoUpload(file) {
673
  }
674
  };
675
 
676
- // ๆทปๅŠ ๆŽงๅˆถๆŒ‰้’ฎๅˆฐ็•Œ้ข
677
  const controlsContainer = document.createElement('div');
678
  controlsContainer.className = 'video-controls';
679
  controlsContainer.appendChild(playPauseBtn);
680
  controlsContainer.appendChild(restartBtn);
681
 
682
- // ๆ‰พๅˆฐๅˆ้€‚็š„ไฝ็ฝฎๆ’ๅ…ฅๆŽงๅˆถๆŒ‰้’ฎ
683
  const container = document.querySelector('.container') || document.body;
684
  container.appendChild(controlsContainer);
685
  };
686
 
687
- // ๆทปๅŠ ้”™่ฏฏๅค„็†
688
  videoElement.onerror = () => {
689
- console.error('่ง†้ข‘ๅŠ ่ฝฝๅคฑ่ดฅ');
690
- alert('่ง†้ข‘ๅŠ ่ฝฝๅคฑ่ดฅ๏ผŒ่ฏทๅฐ่ฏ•ๅ…ถไป–่ง†้ข‘ๆ–‡ไปถ');
691
  };
692
  }
693
 
694
- // ๆทปๅŠ ไธ€ไบ›ๅŸบๆœฌ็š„ๆ ทๅผ
695
  const style = document.createElement('style');
696
  style.textContent = `
697
  .video-controls {
@@ -723,81 +777,81 @@ document.head.appendChild(style);
723
  const holistic = new mpHolistic.Holistic(config);
724
  holistic.onResults(onResults);
725
 
726
- // ๅ‘ˆ็Žฐไธ€ไธชๆŽงๅˆถ้ขๆฟ๏ผŒ็”จๆˆทๅฏไปฅ้€š่ฟ‡ๅฎƒๆ“ไฝœ่งฃๅ†ณๆ–นๆกˆ้€‰้กนใ€‚
727
  new controls
728
  .ControlPanel(controlsElement, {
729
- selfieMode: true,
730
- modelComplexity: 1,
731
- smoothLandmarks: true,
732
- enableSegmentation: false,
733
- smoothSegmentation: true,
734
- minDetectionConfidence: 0.5,
735
- minTrackingConfidence: 0.5,
736
- effect: 'background',
737
- })
738
  .add([
739
- new controls.StaticText({ title: 'MediaPipe ๅ…จ่บซๅงฟๆ€ๆฃ€ๆต‹' }),
740
- fpsControl,
741
- new controls.Toggle({ title: '่‡ชๆ‹ๆจกๅผ', field: 'selfieMode' }),
742
- new controls.SourcePicker({
743
- onSourceChanged: () => {
744
- // ้‡็ฝฎ๏ผŒๅ› ไธบๅœจๆบๆ›ดๆ”นไน‹้—ด้‡็ฝฎๆ—ถ๏ผŒๅงฟๅŠฟไผš็ป™ๅ‡บๆ›ดๅฅฝ็š„็ป“ๆžœใ€‚
745
- holistic.reset();
746
- },
747
- onFrame: async (input, size) => {
748
- const aspect = size.height / size.width;
749
- let width, height;
750
- if (window.innerWidth > window.innerHeight) {
751
- height = window.innerHeight;
752
- width = height / aspect;
753
- }
754
- else {
755
- width = window.innerWidth;
756
- height = width * aspect;
757
- }
758
- canvasElement.width = width;
759
- canvasElement.height = height;
760
- await holistic.send({ image: input });
761
- },
762
- }),
763
- new controls.Slider({
764
- title: 'ๆจกๅž‹ๅคๆ‚ๅบฆ',
765
- field: 'modelComplexity',
766
- discrete: ['่ฝป้‡', 'ๅฎŒๆ•ด', '้‡ๅบฆ'],
767
- }),
768
- new controls.Toggle({ title: 'ๅนณๆป‘ๅ…ณ้”ฎ็‚น', field: 'smoothLandmarks' }),
769
- new controls.Toggle({ title: 'ๅฏ็”จๅˆ†ๅ‰ฒ', field: 'enableSegmentation' }),
770
- new controls.Toggle({ title: 'ๅนณๆป‘ๅˆ†ๅ‰ฒ', field: 'smoothSegmentation' }),
771
- new controls.Slider({
772
- title: 'ๆœ€ๅฐๆฃ€ๆต‹็ฝฎไฟกๅบฆ',
773
- field: 'minDetectionConfidence',
774
- range: [0, 1],
775
- step: 0.01
776
- }),
777
- new controls.Slider({
778
- title: 'ๆœ€ๅฐ่ทŸ่ธช็ฝฎไฟกๅบฆ',
779
- field: 'minTrackingConfidence',
780
- range: [0, 1],
781
- step: 0.01
782
- }),
783
- new controls.Slider({
784
- title: 'ๆ•ˆๆžœ',
785
- field: 'effect',
786
- discrete: { 'background': '่ƒŒๆ™ฏ', 'mask': 'ๅ‰ๆ™ฏ' },
787
- }),
788
- ])
789
  .on(x => {
790
- const options = x;
791
- videoElement.classList.toggle('selfie', options.selfieMode);
792
- activeEffect = x['effect'];
793
- holistic.setOptions(options);
794
- });
795
- // ๅˆๅง‹ๅŒ–ๅ‡ฝๆ•ฐ
 
796
  function initialize() {
797
- // ๅˆๅง‹ๅŒ–ๅ›พ่กจ
798
  initCharts();
799
 
800
- // ่ฎพ็ฝฎ่ง†้ข‘ไธŠไผ ๅค„็†
801
  const videoUploadInput = document.querySelector('#video-upload');
802
  if (videoUploadInput) {
803
  videoUploadInput.addEventListener('change', (e) => {
@@ -807,17 +861,17 @@ function initialize() {
807
  });
808
  }
809
 
810
- // ๅˆๅง‹ๅŒ–ๅงฟๆ€ๆฃ€ๆต‹
811
  const holistic = new mpHolistic.Holistic(config);
812
  holistic.onResults(onResults);
813
 
814
- // ... ไฟๆŒๅ…ถไป–ๅŽŸๆœ‰็š„ๅˆๅง‹ๅŒ–้€ป่พ‘ ...
815
  }
816
 
817
- // ๅฏๅŠจๅบ”็”จ
818
  window.addEventListener('load', initialize);
819
 
820
- // ไฟๆŒๅŽŸๆœ‰็š„็ช—ๅฃๅคงๅฐ่ฐƒๆ•ด้€ป่พ‘
821
  window.addEventListener('resize', () => {
822
  const aspect = videoElement.videoHeight / videoElement.videoWidth;
823
  let width, height;
@@ -831,7 +885,7 @@ window.addEventListener('resize', () => {
831
  canvasElement.width = width;
832
  canvasElement.height = height;
833
 
834
- // ้‡ๆ–ฐ่ฐƒๆ•ดๅ›พ่กจๅคงๅฐ
835
  speedChart.resize();
836
  accelerationChart.resize();
837
- });
 
1
  import DeviceDetector from "https://cdn.skypack.dev/[email protected]";
2
+ // ์‚ฌ์šฉ ๋ฐฉ๋ฒ•: testSupport({client?: string, os?: string}[])
3
+ // client์™€ os๋Š” ์ •๊ทœ ํ‘œํ˜„์‹์ž…๋‹ˆ๋‹ค.
4
+ // ์ฐธ๊ณ : https://cdn.jsdelivr.net/npm/[email protected]/README.md
5
+ // client์™€ os์˜ ์œ ํšจ ๊ฐ’ ํ™•์ธ
 
6
 
7
+ // ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž„ํฌํŠธ
8
+
9
+ // ์†๋„์™€ ๊ฐ€์†๋„ ์ฐจํŠธ๋ฅผ ์ดˆ๊ธฐํ™”
10
  let speedChart, accelerationChart;
11
  let previousPoseData = null;
12
  let lastTimestamp = 0;
13
+
14
  testSupport([
15
  { client: 'Chrome' },
16
  ]);
17
 
18
+ // ์ฐจํŠธ ๊ด€๋ จ ์ƒ์ˆ˜ ์„ค์ •
19
  const CHART_CONFIG = {
20
  maxDataPoints: 50,
21
+ updateInterval: 100, // ์ฐจํŠธ ์—…๋ฐ์ดํŠธ ๊ฐ„๊ฒฉ(ms)
22
  colors: {
23
  speed: {
24
  primary: 'rgba(75, 192, 192, 1)',
 
31
  }
32
  };
33
 
34
+ // ์ฐจํŠธ ์ดˆ๊ธฐํ™”
35
  function initCharts() {
36
+ // ์ฐจํŠธ๋ฅผ ๋‹ด์„ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ
37
  const chartsContainer = document.createElement('div');
38
  chartsContainer.className = 'charts-container';
39
  document.querySelector('.container').appendChild(chartsContainer);
40
 
41
+ // ์†๋„ ์ฐจํŠธ ์ปจํ…Œ์ด๋„ˆ
42
  const speedChartContainer = document.createElement('div');
43
  speedChartContainer.className = 'chart-card';
44
  const speedCanvas = document.createElement('canvas');
 
46
  speedChartContainer.appendChild(speedCanvas);
47
  chartsContainer.appendChild(speedChartContainer);
48
 
49
+ // ๊ฐ€์†๋„ ์ฐจํŠธ ์ปจํ…Œ์ด๋„ˆ
50
  const accelerationChartContainer = document.createElement('div');
51
  accelerationChartContainer.className = 'chart-card';
52
  const accelerationCanvas = document.createElement('canvas');
 
54
  accelerationChartContainer.appendChild(accelerationCanvas);
55
  chartsContainer.appendChild(accelerationChartContainer);
56
 
57
+ // ์†๋„ ์ฐจํŠธ ์„ค์ •
58
  speedChart = new Chart(speedCanvas.getContext('2d'), {
59
  type: 'line',
60
  data: {
61
  labels: [],
62
  datasets: [{
63
+ label: '์šด๋™ ์†๋„ (ํ”ฝ์…€/์ดˆ)',
64
  data: [],
65
  borderColor: CHART_CONFIG.colors.speed.primary,
66
  backgroundColor: CHART_CONFIG.colors.speed.background,
 
131
  }
132
  });
133
 
134
+ // ๊ฐ€์†๋„ ์ฐจํŠธ ์„ค์ •
135
  accelerationChart = new Chart(accelerationCanvas.getContext('2d'), {
136
  type: 'line',
137
  data: {
138
  labels: [],
139
  datasets: [{
140
+ label: '๊ฐ€์†๋„ (ํ”ฝ์…€/์ดˆยฒ)',
141
  data: [],
142
  borderColor: CHART_CONFIG.colors.acceleration.primary,
143
  backgroundColor: CHART_CONFIG.colors.acceleration.background,
 
208
  }
209
  });
210
 
211
+ // ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ
212
  window.addEventListener('resize', () => {
213
  speedChart.resize();
214
  accelerationChart.resize();
215
  });
216
  }
217
 
218
+ // ์ž์„ธ ๋ณ€ํ™”์˜ ์†๋„์™€ ๊ฐ€์†๋„๋ฅผ ๊ณ„์‚ฐ
219
  function calculateMotionMetrics(results, timestamp) {
220
+ // ๊ธฐ๋ณธ ๊ฒ€์ฆ
221
  if (!results || !results.poseLandmarks || !Array.isArray(results.poseLandmarks)) {
222
  return { speed: 0, acceleration: 0 };
223
  }
224
 
225
+ // ์ดˆ๊ธฐ ์ƒํƒœ
226
  if (!previousPoseData || !previousPoseData.poseLandmarks) {
227
  previousPoseData = {
228
  poseLandmarks: [...results.poseLandmarks]
 
231
  return { speed: 0, acceleration: 0 };
232
  }
233
 
234
+ const deltaTime = (timestamp - lastTimestamp) / 1000; // ์ดˆ ๋‹จ์œ„๋กœ ๋ณ€ํ™˜
235
  if (deltaTime === 0) {
236
  return { speed: 0, acceleration: 0 };
237
  }
238
 
239
+ // ํ‚คํฌ์ธํŠธ ํ‰๊ท  ์ด๋™๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ
240
  let totalDisplacement = 0;
241
  let validPoints = 0;
242
 
243
  try {
244
+ // ์œ ํšจํ•œ ํ‚คํฌ์ธํŠธ๋งŒ ์‚ฌ์šฉ
245
  results.poseLandmarks.forEach((landmark, index) => {
246
  const prevLandmark = previousPoseData.poseLandmarks[index];
247
 
248
+ if (
249
+ landmark && prevLandmark &&
250
  typeof landmark.x === 'number' &&
251
  typeof landmark.y === 'number' &&
252
  typeof prevLandmark.x === 'number' &&
253
  typeof prevLandmark.y === 'number' &&
254
+ // ์„ ํƒ ์‚ฌํ•ญ: ๊ฐ€์‹œ์„ฑ(visibility) ๊ฐ’์ด ์ผ์ • ๊ธฐ์ค€ ์ด์ƒ์ผ ๋•Œ๋งŒ
255
  (!landmark.visibility || landmark.visibility > 0.5) &&
256
+ (!prevLandmark.visibility || prevLandmark.visibility > 0.5)
257
+ ) {
258
  const dx = landmark.x - prevLandmark.x;
259
  const dy = landmark.y - prevLandmark.y;
260
  const displacement = Math.sqrt(dx * dx + dy * dy);
261
 
262
+ // ๋น„์ •์ƒ์ ์œผ๋กœ ํฐ ์ด๋™ ๊ฑฐ๋ฆฌ๋Š” ํ•„ํ„ฐ๋ง
263
+ if (displacement < 1.0) {
264
  totalDisplacement += displacement;
265
  validPoints++;
266
  }
267
  }
268
  });
269
  } catch (error) {
270
+ console.warn('์ด๋™ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜:', error);
271
  return { speed: 0, acceleration: 0 };
272
  }
273
 
274
+ // ์œ ํšจ ํฌ์ธํŠธ๊ฐ€ ์—†๋‹ค๋ฉด 0 ๋ฐ˜ํ™˜
275
  if (validPoints === 0) {
276
  return { speed: 0, acceleration: 0 };
277
  }
278
 
279
+ // ํ‰๊ท  ์ด๋™๊ฑฐ๋ฆฌ ๋ฐ ์†๋„ ๊ณ„์‚ฐ
280
  const averageDisplacement = totalDisplacement / validPoints;
281
  const currentSpeed = averageDisplacement / deltaTime;
282
 
283
+ // ์ด์ „ ์†๋„๊ฐ€ ์—†์œผ๋ฉด 0
284
  let previousSpeed = 0;
285
  try {
286
+ previousSpeed = speedChart.data.datasets[0].data[
287
+ speedChart.data.datasets[0].data.length - 1
288
+ ] || 0;
289
  } catch (error) {
290
+ console.warn('์ด์ „ ์†๋„ ์ ‘๊ทผ ์ค‘ ์˜ค๋ฅ˜:', error);
291
  }
292
 
293
+ // ๊ฐ€์†๋„ ๊ณ„์‚ฐ
294
  const acceleration = (currentSpeed - previousSpeed) / deltaTime;
295
 
296
+ // ๋‹ค์Œ ํ”„๋ ˆ์ž„ ๊ณ„์‚ฐ์„ ์œ„ํ•ด ์ด์ „ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
297
  previousPoseData = {
298
  poseLandmarks: [...results.poseLandmarks]
299
  };
300
  lastTimestamp = timestamp;
301
 
302
+ // ๊ฐ’ ๊ฒ€์ฆ ๋ฐ ์ œํ•œ
303
  const metrics = {
304
  speed: isFinite(currentSpeed) ? Math.min(Math.max(currentSpeed, 0), 1000) : 0,
305
  acceleration: isFinite(acceleration) ? Math.min(Math.max(acceleration, -1000), 1000) : 0
 
308
  return metrics;
309
  }
310
 
311
+ // ์ฐจํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•จ์ˆ˜ (์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ํฌํ•จ)
312
  function updateCharts(metrics) {
313
  if (!metrics || typeof metrics.speed !== 'number' || typeof metrics.acceleration !== 'number') {
314
+ console.warn('์ž˜๋ชป๋œ metrics ๋ฐ์ดํ„ฐ:', metrics);
315
  return;
316
  }
317
 
318
  try {
319
+ const timestamp = new Date().toLocaleTimeString('ko-KR', {
320
  hour: '2-digit',
321
  minute: '2-digit',
322
  second: '2-digit'
323
  });
324
 
325
+ // ์ฐจํŠธ ๊ฐ์ฒด๊ฐ€ ์ œ๋Œ€๋กœ ์ดˆ๊ธฐํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
326
  if (!speedChart || !speedChart.data || !speedChart.data.labels) {
327
+ console.warn('์†๋„ ์ฐจํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.');
328
  return;
329
  }
330
  if (!accelerationChart || !accelerationChart.data || !accelerationChart.data.labels) {
331
+ console.warn('๊ฐ€์†๋„ ์ฐจํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.');
332
  return;
333
  }
334
 
335
+ // ์†๋„ ์ฐจํŠธ ์—…๋ฐ์ดํŠธ
336
  speedChart.data.labels.push(timestamp);
337
  speedChart.data.datasets[0].data.push(metrics.speed);
338
  if (speedChart.data.labels.length > CHART_CONFIG.maxDataPoints) {
 
340
  speedChart.data.datasets[0].data.shift();
341
  }
342
 
343
+ // ๊ฐ€์†๋„ ์ฐจํŠธ ์—…๋ฐ์ดํŠธ
344
  accelerationChart.data.labels.push(timestamp);
345
  accelerationChart.data.datasets[0].data.push(metrics.acceleration);
346
  if (accelerationChart.data.labels.length > CHART_CONFIG.maxDataPoints) {
 
348
  accelerationChart.data.datasets[0].data.shift();
349
  }
350
 
351
+ // requestAnimationFrame์„ ์‚ฌ์šฉํ•ด ์ฐจํŠธ ์—…๋ฐ์ดํŠธ ์ตœ์ ํ™”
352
  requestAnimationFrame(() => {
353
  try {
354
  speedChart.update('none');
355
  accelerationChart.update('none');
356
  } catch (error) {
357
+ console.warn('์ฐจํŠธ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:', error);
358
  }
359
  });
360
  } catch (error) {
361
+ console.warn('updateCharts์—์„œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
362
  }
363
  }
364
 
 
365
  function testSupport(supportedDevices) {
366
  const deviceDetector = new DeviceDetector();
367
  const detectedDevice = deviceDetector.parse(navigator.userAgent);
 
383
  break;
384
  }
385
  if (!isSupported) {
386
+ alert(`์ด ๋ฐ๋ชจ๋Š” ${detectedDevice.client.name}/${detectedDevice.os.name} ์—์„œ ์‹คํ–‰๋  ๋•Œ ` +
387
+ `์™„์ „ํžˆ ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ณ„์† ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ๋ณธ์ธ ์ฑ…์ž„ ํ•˜์— ์ง„ํ–‰ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.`);
388
  }
389
  }
390
 
 
396
  `${mpHolistic.VERSION}/${file}`;
397
  } };
398
 
399
+ // ์ž…๋ ฅ ํ”„๋ ˆ์ž„์€ ์—ฌ๊ธฐ์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
400
  const videoElement = document.getElementsByClassName('input_video')[0];
401
  const canvasElement = document.getElementsByClassName('output_canvas')[0];
402
  const controlsElement = document.getElementsByClassName('control-panel')[0];
403
  const canvasCtx = canvasElement.getContext('2d');
404
 
405
+ // ์ดํ›„ tick()์ด ํ˜ธ์ถœ๋  ๋•Œ๋งˆ๋‹ค ์ฐธ์กฐํ•  FPS ์ปจํŠธ๋กค
 
406
  const fpsControl = new controls.FPS();
407
 
408
+ // ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ๋ฅผ ์ˆจ๊ธฐ๋Š” ์ตœ์ ํ™”
409
  const spinner = document.querySelector('.loading');
410
  spinner.ontransitionend = () => {
411
  spinner.style.display = 'none';
 
419
 
420
  function removeLandmarks(results) {
421
  if (results.poseLandmarks) {
422
+ removeElements(results.poseLandmarks, [
423
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
424
+ 15, 16, 17, 18, 19, 20, 21, 22
425
+ ]);
426
  }
427
  }
428
 
 
446
 
447
  let activeEffect = 'mask';
448
 
 
449
  function onResults(results) {
450
+ // ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ์ˆจ๊ธฐ๊ธฐ
451
  document.body.classList.add('loaded');
452
 
453
+ // ๊ทธ๋ฆด ํ•„์š” ์—†๋Š” ๋žœ๋“œ๋งˆํฌ ์ œ๊ฑฐ
454
  removeLandmarks(results);
455
 
456
+ // FPS ์—…๋ฐ์ดํŠธ
457
  fpsControl.tick();
458
 
459
+ // ์บ”๋ฒ„์Šค์— ๊ทธ๋ฆฌ๊ธฐ
460
  canvasCtx.save();
461
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
462
 
463
  if (results.segmentationMask) {
464
+ canvasCtx.drawImage(
465
+ results.segmentationMask,
466
+ 0, 0, canvasElement.width, canvasElement.height
467
+ );
468
 
469
+ // ๊ธฐ์กด ํ”ฝ์…€๋งŒ ๋ฎ์–ด์“ฐ๊ธฐ
470
  if (activeEffect === 'mask' || activeEffect === 'both') {
471
  canvasCtx.globalCompositeOperation = 'source-in';
472
  canvasCtx.fillStyle = '#00FF007F';
 
477
  canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
478
  }
479
 
480
+ // ๋ˆ„๋ฝ๋œ ํ”ฝ์…€๋งŒ ๋ฎ์–ด์“ฐ๊ธฐ
481
  canvasCtx.globalCompositeOperation = 'destination-atop';
482
  canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
483
  canvasCtx.globalCompositeOperation = 'source-over';
 
485
  canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
486
  }
487
 
488
+ // ๋™์ž‘ ์ง€ํ‘œ ๊ณ„์‚ฐ ํ›„ ์ฐจํŠธ ์—…๋ฐ์ดํŠธ
489
  const metrics = calculateMotionMetrics(results, performance.now());
490
  updateCharts(metrics);
491
 
492
+ // ์•ˆ์ „ ๊ฒ€์‚ฌ
493
  if (!results.poseLandmarks || !mpHolistic.POSE_LANDMARKS) {
494
  canvasCtx.restore();
495
  return;
496
  }
497
 
498
+ // ํŒ”๊ฟˆ์น˜์—์„œ ์†๊นŒ์ง€ ์—ฐ๊ฒฐ
499
  canvasCtx.lineWidth = 5;
500
  if (results.poseLandmarks) {
501
  if (results.rightHandLandmarks && results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW]) {
 
514
  }
515
  }
516
 
517
+ // ์ „์‹  ์ž์„ธ ์—ฐ๊ฒฐ
518
  if (results.poseLandmarks && mpHolistic.POSE_CONNECTIONS) {
519
+ drawingUtils.drawConnectors(
520
+ canvasCtx,
521
+ results.poseLandmarks,
522
+ mpHolistic.POSE_CONNECTIONS,
523
+ { color: 'white' }
524
+ );
525
  }
526
 
527
+ // ์™ผ์ชฝ ์ž์„ธ ๋žœ๋“œ๋งˆํฌ
528
  if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_LEFT) {
529
  const leftLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_LEFT)
530
  .map(index => results.poseLandmarks[index])
531
  .filter(landmark => landmark !== undefined);
532
 
533
  if (leftLandmarks.length > 0) {
534
+ drawingUtils.drawLandmarks(canvasCtx, leftLandmarks, {
535
+ visibilityMin: 0.65,
536
+ color: 'white',
537
+ fillColor: 'rgb(255,138,0)'
538
+ });
539
  }
540
  }
541
 
542
+ // ์˜ค๋ฅธ์ชฝ ์ž์„ธ ๋žœ๋“œ๋งˆํฌ
543
  if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_RIGHT) {
544
  const rightLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_RIGHT)
545
  .map(index => results.poseLandmarks[index])
546
  .filter(landmark => landmark !== undefined);
547
 
548
  if (rightLandmarks.length > 0) {
549
+ drawingUtils.drawLandmarks(canvasCtx, rightLandmarks, {
550
+ visibilityMin: 0.65,
551
+ color: 'white',
552
+ fillColor: 'rgb(0,217,231)'
553
+ });
554
  }
555
  }
556
 
557
+ // ์† ๋žœ๋“œ๋งˆํฌ
558
  if (results.rightHandLandmarks && mpHolistic.HAND_CONNECTIONS) {
559
+ drawingUtils.drawConnectors(
560
+ canvasCtx,
561
+ results.rightHandLandmarks,
562
+ mpHolistic.HAND_CONNECTIONS,
563
+ { color: 'white' }
564
+ );
565
  drawingUtils.drawLandmarks(canvasCtx, results.rightHandLandmarks, {
566
  color: 'white',
567
  fillColor: 'rgb(0,217,231)',
 
573
  }
574
 
575
  if (results.leftHandLandmarks && mpHolistic.HAND_CONNECTIONS) {
576
+ drawingUtils.drawConnectors(
577
+ canvasCtx,
578
+ results.leftHandLandmarks,
579
+ mpHolistic.HAND_CONNECTIONS,
580
+ { color: 'white' }
581
+ );
582
  drawingUtils.drawLandmarks(canvasCtx, results.leftHandLandmarks, {
583
  color: 'white',
584
  fillColor: 'rgb(255,138,0)',
 
589
  });
590
  }
591
 
592
+ // ์–ผ๊ตด ๋žœ๋“œ๋งˆํฌ
593
  if (results.faceLandmarks && mpHolistic.FACEMESH_TESSELATION) {
594
+ drawingUtils.drawConnectors(
595
+ canvasCtx,
596
+ results.faceLandmarks,
597
+ mpHolistic.FACEMESH_TESSELATION,
598
+ { color: '#C0C0C070', lineWidth: 1 }
599
+ );
600
  if (mpHolistic.FACEMESH_RIGHT_EYE) {
601
+ drawingUtils.drawConnectors(
602
+ canvasCtx,
603
+ results.faceLandmarks,
604
+ mpHolistic.FACEMESH_RIGHT_EYE,
605
+ { color: 'rgb(0,217,231)' }
606
+ );
607
  }
608
  if (mpHolistic.FACEMESH_RIGHT_EYEBROW) {
609
+ drawingUtils.drawConnectors(
610
+ canvasCtx,
611
+ results.faceLandmarks,
612
+ mpHolistic.FACEMESH_RIGHT_EYEBROW,
613
+ { color: 'rgb(0,217,231)' }
614
+ );
615
  }
616
  if (mpHolistic.FACEMESH_LEFT_EYE) {
617
+ drawingUtils.drawConnectors(
618
+ canvasCtx,
619
+ results.faceLandmarks,
620
+ mpHolistic.FACEMESH_LEFT_EYE,
621
+ { color: 'rgb(255,138,0)' }
622
+ );
623
  }
624
  if (mpHolistic.FACEMESH_LEFT_EYEBROW) {
625
+ drawingUtils.drawConnectors(
626
+ canvasCtx,
627
+ results.faceLandmarks,
628
+ mpHolistic.FACEMESH_LEFT_EYEBROW,
629
+ { color: 'rgb(255,138,0)' }
630
+ );
631
  }
632
  if (mpHolistic.FACEMESH_FACE_OVAL) {
633
+ drawingUtils.drawConnectors(
634
+ canvasCtx,
635
+ results.faceLandmarks,
636
+ mpHolistic.FACEMESH_FACE_OVAL,
637
+ { color: '#E0E0E0', lineWidth: 5 }
638
+ );
639
  }
640
  if (mpHolistic.FACEMESH_LIPS) {
641
+ drawingUtils.drawConnectors(
642
+ canvasCtx,
643
+ results.faceLandmarks,
644
+ mpHolistic.FACEMESH_LIPS,
645
+ { color: '#E0E0E0', lineWidth: 5 }
646
+ );
647
  }
648
  }
649
 
650
  canvasCtx.restore();
651
  }
652
+
653
+ // ์˜์ƒ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
654
  function handleVideoUpload(file) {
655
+ // ๋น„๋””์˜ค URL ์ƒ์„ฑ
656
  const videoUrl = URL.createObjectURL(file);
657
 
658
+ // ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”
659
  speedChart.data.labels = [];
660
  speedChart.data.datasets[0].data = [];
661
  accelerationChart.data.labels = [];
 
663
  previousPoseData = null;
664
  lastTimestamp = 0;
665
 
666
+ // ์ž์„ธ ๊ฐ์ง€ ๋ฆฌ์…‹
667
  holistic.reset();
668
 
669
+ // ๋น„๋””์˜ค ์†Œ์Šค ์—…๋ฐ์ดํŠธ
670
  videoElement.src = videoUrl;
671
 
672
+ // ๋น„๋””์˜ค ๋กœ๋“œ ์™„๋ฃŒ ํ›„ ์ฒ˜๋ฆฌ
673
  videoElement.onloadedmetadata = () => {
674
+ // ๋น„๋””์˜ค ๋น„์œจ์— ๋งž์ถฐ ์บ”๋ฒ„์Šค ์‚ฌ์ด์ฆˆ ์กฐ์ •
675
  const aspect = videoElement.videoHeight / videoElement.videoWidth;
676
  let width, height;
677
  if (window.innerWidth > window.innerHeight) {
 
684
  canvasElement.width = width;
685
  canvasElement.height = height;
686
 
687
+ // ์˜์ƒ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ animation frame ์ƒ์„ฑ
688
  let animationId;
689
 
690
  async function processFrame() {
 
692
  cancelAnimationFrame(animationId);
693
  return;
694
  }
695
+ // ํ˜„์žฌ ํ”„๋ ˆ์ž„์„ ์ž์„ธ ๊ฐ์ง€์— ์ „์†ก
 
696
  await holistic.send({
697
  image: videoElement
698
  });
 
 
699
  animationId = requestAnimationFrame(processFrame);
700
  }
701
 
702
+ // ๋น„๋””์˜ค ์žฌ์ƒ ์ฒ˜๋ฆฌ
703
  videoElement.onplay = () => {
704
  processFrame();
705
  };
706
 
707
+ // ์žฌ์ƒ/์ผ์‹œ์ •์ง€ ๋ฒ„ํŠผ
708
  const playPauseBtn = document.createElement('button');
709
+ playPauseBtn.textContent = '์žฌ์ƒ/์ผ์‹œ์ •์ง€';
710
  playPauseBtn.className = 'control-button';
711
  playPauseBtn.onclick = () => {
712
  if (videoElement.paused) {
 
716
  }
717
  };
718
 
719
+ // ๋‹ค์‹œ ์‹œ์ž‘ ๋ฒ„ํŠผ
720
  const restartBtn = document.createElement('button');
721
+ restartBtn.textContent = '์ฒ˜์Œ๋ถ€ํ„ฐ ์žฌ์ƒ';
722
  restartBtn.className = 'control-button';
723
  restartBtn.onclick = () => {
724
  videoElement.currentTime = 0;
 
727
  }
728
  };
729
 
730
+ // ๋ฒ„ํŠผ์„ ํ™”๋ฉด์— ์ถ”๊ฐ€
731
  const controlsContainer = document.createElement('div');
732
  controlsContainer.className = 'video-controls';
733
  controlsContainer.appendChild(playPauseBtn);
734
  controlsContainer.appendChild(restartBtn);
735
 
736
+ // ์ ์ ˆํ•œ ์œ„์น˜์— ๋ฒ„ํŠผ ์‚ฝ์ž…
737
  const container = document.querySelector('.container') || document.body;
738
  container.appendChild(controlsContainer);
739
  };
740
 
741
+ // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
742
  videoElement.onerror = () => {
743
+ console.error('๋น„๋””์˜ค ๋กœ๋“œ ์‹คํŒจ');
744
+ alert('๋น„๋””์˜ค ๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋น„๋””์˜ค ํŒŒ์ผ์„ ์‹œ๋„ํ•ด๋ณด์„ธ์š”.');
745
  };
746
  }
747
 
748
+ // ์ผ๋ถ€ ๊ธฐ๋ณธ ์Šคํƒ€์ผ ์ถ”๊ฐ€
749
  const style = document.createElement('style');
750
  style.textContent = `
751
  .video-controls {
 
777
  const holistic = new mpHolistic.Holistic(config);
778
  holistic.onResults(onResults);
779
 
780
+ // ์ปจํŠธ๋กค ํŒจ๋„์„ ๊ทธ๋ ค์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ต์…˜ ์ œ์–ด ์ œ๊ณต
781
  new controls
782
  .ControlPanel(controlsElement, {
783
+ selfieMode: true,
784
+ modelComplexity: 1,
785
+ smoothLandmarks: true,
786
+ enableSegmentation: false,
787
+ smoothSegmentation: true,
788
+ minDetectionConfidence: 0.5,
789
+ minTrackingConfidence: 0.5,
790
+ effect: 'background',
791
+ })
792
  .add([
793
+ new controls.StaticText({ title: 'MediaPipe ์ „์‹  ์ž์„ธ ๊ฐ์ง€' }),
794
+ fpsControl,
795
+ new controls.Toggle({ title: '์…€ํ”ผ ๋ชจ๋“œ', field: 'selfieMode' }),
796
+ new controls.SourcePicker({
797
+ onSourceChanged: () => {
798
+ // ์†Œ์Šค ๋ณ€๊ฒฝ ์‹œ ๋ฆฌ์…‹. ๋ฆฌ์…‹ ํ›„ ์ž์„ธ๋ฅผ ๋” ์ž˜ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Œ
799
+ holistic.reset();
800
+ },
801
+ onFrame: async (input, size) => {
802
+ const aspect = size.height / size.width;
803
+ let width, height;
804
+ if (window.innerWidth > window.innerHeight) {
805
+ height = window.innerHeight;
806
+ width = height / aspect;
807
+ } else {
808
+ width = window.innerWidth;
809
+ height = width * aspect;
810
+ }
811
+ canvasElement.width = width;
812
+ canvasElement.height = height;
813
+ await holistic.send({ image: input });
814
+ },
815
+ }),
816
+ new controls.Slider({
817
+ title: '๋ชจ๋ธ ๋ณต์žก๋„',
818
+ field: 'modelComplexity',
819
+ discrete: ['๊ฒฝ๋Ÿ‰', '์™„์ „', '๊ณ ๊ธ‰'],
820
+ }),
821
+ new controls.Toggle({ title: '๋žœ๋“œ๋งˆํฌ ํ‰ํ™œํ™”', field: 'smoothLandmarks' }),
822
+ new controls.Toggle({ title: '์„ธ๊ทธ๋จผํ…Œ์ด์…˜ ์‚ฌ์šฉ', field: 'enableSegmentation' }),
823
+ new controls.Toggle({ title: '์„ธ๊ทธ๋จผํ…Œ์ด์…˜ ํ‰ํ™œํ™”', field: 'smoothSegmentation' }),
824
+ new controls.Slider({
825
+ title: '์ตœ์†Œ ๊ฐ์ง€ ์‹ ๋ขฐ๋„',
826
+ field: 'minDetectionConfidence',
827
+ range: [0, 1],
828
+ step: 0.01
829
+ }),
830
+ new controls.Slider({
831
+ title: '์ตœ์†Œ ์ถ”์  ์‹ ๋ขฐ๋„',
832
+ field: 'minTrackingConfidence',
833
+ range: [0, 1],
834
+ step: 0.01
835
+ }),
836
+ new controls.Slider({
837
+ title: 'ํšจ๊ณผ',
838
+ field: 'effect',
839
+ discrete: { 'background': '๋ฐฐ๊ฒฝ', 'mask': '์ „๊ฒฝ' },
840
+ }),
841
+ ])
 
842
  .on(x => {
843
+ const options = x;
844
+ videoElement.classList.toggle('selfie', options.selfieMode);
845
+ activeEffect = x['effect'];
846
+ holistic.setOptions(options);
847
+ });
848
+
849
+ // ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
850
  function initialize() {
851
+ // ์ฐจํŠธ ์ดˆ๊ธฐํ™”
852
  initCharts();
853
 
854
+ // ๋น„๋””์˜ค ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
855
  const videoUploadInput = document.querySelector('#video-upload');
856
  if (videoUploadInput) {
857
  videoUploadInput.addEventListener('change', (e) => {
 
861
  });
862
  }
863
 
864
+ // ์ž์„ธ ๊ฐ์ง€ ์ดˆ๊ธฐํ™”
865
  const holistic = new mpHolistic.Holistic(config);
866
  holistic.onResults(onResults);
867
 
868
+ // ... ๊ธฐ์กด ์ดˆ๊ธฐํ™” ๋กœ์ง์„ ์œ ์ง€ ...
869
  }
870
 
871
+ // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘
872
  window.addEventListener('load', initialize);
873
 
874
+ // ์ฐฝ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ
875
  window.addEventListener('resize', () => {
876
  const aspect = videoElement.videoHeight / videoElement.videoWidth;
877
  let width, height;
 
885
  canvasElement.width = width;
886
  canvasElement.height = height;
887
 
888
+ // ์ฐจํŠธ ํฌ๊ธฐ ์žฌ์กฐ์ •
889
  speedChart.resize();
890
  accelerationChart.resize();
891
+ });