Skip to content

Commit d179988

Browse files
authored
Create app.html
1 parent 271003f commit d179988

File tree

1 file changed

+342
-0
lines changed

1 file changed

+342
-0
lines changed

app.html

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
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">
6+
<title>Regression Coefficient Visualizer</title>
7+
<link href="https://fonts.googleapis.com/css2?family=Xanh+Mono:ital@0;1&display=swap" rel="stylesheet">
8+
<script src="https://cdn.tailwindcss.com"></script>
9+
<style>
10+
body {
11+
font-family: 'Xanh Mono', monospace;
12+
background-color: #111827; /* Dark background */
13+
}
14+
.container {
15+
position: relative;
16+
width: 100%;
17+
max-width: 640px;
18+
margin: auto;
19+
border: 2px solid #4b5563;
20+
border-radius: 0.5rem;
21+
overflow: hidden;
22+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4);
23+
}
24+
#webcam {
25+
width: 100%;
26+
height: auto;
27+
display: block;
28+
transform: scaleX(-1);
29+
}
30+
#overlayCanvas {
31+
position: absolute;
32+
top: 0;
33+
left: 0;
34+
width: 100%;
35+
height: 100%;
36+
pointer-events: none;
37+
transform: scaleX(-1);
38+
}
39+
.loader-container {
40+
display: flex;
41+
flex-direction: column;
42+
align-items: center;
43+
justify-content: center;
44+
height: 480px;
45+
color: #d1d5db;
46+
}
47+
.loader {
48+
border: 8px solid #374151;
49+
border-radius: 50%;
50+
border-top: 8px solid #3b82f6;
51+
width: 60px;
52+
height: 60px;
53+
animation: spin 2s linear infinite;
54+
}
55+
@keyframes spin {
56+
0% { transform: rotate(0deg); }
57+
100% { transform: rotate(360deg); }
58+
}
59+
.controls {
60+
display: grid;
61+
grid-template-columns: repeat(2, 1fr);
62+
gap: 1rem;
63+
margin-bottom: 1rem;
64+
color: #d1d5db;
65+
max-width: 600px;
66+
margin-left: auto;
67+
margin-right: auto;
68+
}
69+
.control-group {
70+
display: flex;
71+
justify-content: center;
72+
align-items: center;
73+
gap: 0.75rem;
74+
background-color: #1f2937;
75+
padding: 0.5rem;
76+
border-radius: 0.375rem;
77+
}
78+
.controls label {
79+
cursor: pointer;
80+
}
81+
.controls input[type="checkbox"] {
82+
margin-right: 0.5rem;
83+
accent-color: #3b82f6;
84+
width: 16px;
85+
height: 16px;
86+
}
87+
.controls input[type="range"] {
88+
accent-color: #3b82f6;
89+
flex-grow: 1;
90+
}
91+
.controls select {
92+
background-color: #374151;
93+
border: 1px solid #4b5563;
94+
border-radius: 0.25rem;
95+
padding: 0.25rem;
96+
}
97+
</style>
98+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
99+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
100+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
101+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
102+
</head>
103+
<body class="bg-gray-900 text-white flex items-center justify-center min-h-screen p-4">
104+
105+
<div class="max-w-3xl w-full mx-auto p-4 md:p-8">
106+
<h1 class="text-2xl font-bold text-center mb-2">Regression Coefficient Visualizer</h1>
107+
<p id="instruction-text" class="text-center text-gray-400 mb-4">Clap + show hands to begin...</p>
108+
109+
<div id="controls" class="controls" style="display: none;">
110+
<div class="control-group">
111+
<label for="x-scale-select">X-Axis:</label>
112+
<select id="x-scale-select">
113+
<option value="0_10">0 to 10</option>
114+
<option value="0_100" selected>0 to 100</option>
115+
<option value="-1_1">-1 to 1</option>
116+
</select>
117+
</div>
118+
<div class="control-group">
119+
<label for="y-scale-select">Y-Axis:</label>
120+
<select id="y-scale-select">
121+
<option value="0_10">0 to 10</option>
122+
<option value="0_100" selected>0 to 100</option>
123+
<option value="-1_1">-1 to 1</option>
124+
</select>
125+
</div>
126+
<div class="control-group">
127+
<label for="show-data">
128+
<input type="checkbox" id="show-data" checked> Data Points
129+
</label>
130+
</div>
131+
<div class="control-group">
132+
<label for="precision-slider">Precision</label>
133+
<input type="range" id="precision-slider" min="20" max="190" value="80">
134+
</div>
135+
</div>
136+
137+
<div id="loading-container" class="loader-container">
138+
<div class="loader"></div>
139+
<p class="mt-4 text-lg">Starting camera...</p>
140+
</div>
141+
142+
<div class="container" style="display: none;">
143+
<video id="webcam" autoplay playsinline></video>
144+
<canvas id="overlayCanvas"></canvas>
145+
</div>
146+
147+
<div class="text-center mt-4">
148+
<a href="https://github.com/dbann/regression_visualizer" target="_blank" rel="noopener noreferrer" class="text-xs text-gray-500 hover:text-gray-400 transition-colors">
149+
code/feedback: github.com/dbann/regression_visualizer
150+
</a>
151+
</div>
152+
</div>
153+
154+
<script type="module">
155+
// --- DOM Elements ---
156+
const videoElement = document.getElementById('webcam');
157+
const canvasElement = document.getElementById('overlayCanvas');
158+
const canvasCtx = canvasElement.getContext('2d');
159+
const loadingContainer = document.getElementById('loading-container');
160+
const container = document.querySelector('.container');
161+
const instructionText = document.getElementById('instruction-text');
162+
const controlsContainer = document.getElementById('controls');
163+
const showDataCheckbox = document.getElementById('show-data');
164+
const precisionSlider = document.getElementById('precision-slider');
165+
const xScaleSelect = document.getElementById('x-scale-select');
166+
const yScaleSelect = document.getElementById('y-scale-select');
167+
168+
// --- State Variables ---
169+
let showDataPoints = true;
170+
let scatterPoints = [];
171+
let xScale = { min: 0, max: 100 };
172+
let yScale = { min: 0, max: 100 };
173+
174+
// --- Event Listeners ---
175+
showDataCheckbox.addEventListener('change', (e) => showDataPoints = e.target.checked);
176+
precisionSlider.addEventListener('input', (e) => {
177+
const maxError = 200 - parseInt(e.target.value);
178+
scatterPoints = generateScatterPoints(maxError);
179+
});
180+
const parseScale = (scaleString) => {
181+
const [min, max] = scaleString.split('_').map(Number);
182+
return { min, max };
183+
};
184+
xScaleSelect.addEventListener('change', (e) => xScale = parseScale(e.target.value));
185+
yScaleSelect.addEventListener('change', (e) => yScale = parseScale(e.target.value));
186+
187+
// --- Initial Setup ---
188+
function generateScatterPoints(errorMagnitude) {
189+
const points = [];
190+
const numPoints = 70;
191+
for (let i = 0; i < numPoints; i++) {
192+
points.push({
193+
xRatio: Math.random(),
194+
error: (Math.random() - 0.5) * errorMagnitude * 2
195+
});
196+
}
197+
return points;
198+
}
199+
scatterPoints = generateScatterPoints(200 - parseInt(precisionSlider.value));
200+
201+
// --- Main Drawing Function ---
202+
function onResults(results) {
203+
canvasElement.width = videoElement.videoWidth;
204+
canvasElement.height = videoElement.videoHeight;
205+
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
206+
207+
if (results.multiHandLandmarks && results.multiHandLandmarks.length === 2) {
208+
instructionText.style.display = 'none';
209+
drawRegressionVisualizer(results.multiHandLandmarks);
210+
} else {
211+
instructionText.style.display = 'block';
212+
}
213+
}
214+
215+
// --- Drawing Logic ---
216+
function drawDataPoints(p1, p2) {
217+
canvasCtx.fillStyle = 'rgba(59, 130, 246, 0.5)';
218+
const dx = p2.x - p1.x;
219+
const dy = p2.y - p1.y;
220+
221+
scatterPoints.forEach(point => {
222+
const lineX = p1.x + dx * point.xRatio;
223+
const lineY = p1.y + dy * point.xRatio;
224+
const finalY = lineY + point.error;
225+
canvasCtx.beginPath();
226+
canvasCtx.arc(lineX, finalY, 4, 0, 2 * Math.PI);
227+
canvasCtx.fill();
228+
});
229+
}
230+
231+
// Maps a value from one range to another
232+
function mapRange(value, inMin, inMax, outMin, outMax) {
233+
return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
234+
}
235+
236+
function drawRegressionVisualizer(hands) {
237+
const [hand1Landmarks, hand2Landmarks] = hands;
238+
const palmIndices = [0, 5, 9, 13, 17];
239+
const palm1_norm = getAveragePosition(hand1Landmarks, palmIndices);
240+
const palm2_norm = getAveragePosition(hand2Landmarks, palmIndices);
241+
if (!palm1_norm || !palm2_norm) return;
242+
243+
// Correctly identify left and right hands on the mirrored screen
244+
const screenLeftHand_norm = palm1_norm.x < palm2_norm.x ? palm1_norm : palm2_norm;
245+
const screenRightHand_norm = palm1_norm.x < palm2_norm.x ? palm2_norm : palm1_norm;
246+
247+
// p1 and p2 are the raw pixel coordinates for drawing on the canvas
248+
const p1 = { x: screenLeftHand_norm.x * canvasElement.width, y: screenLeftHand_norm.y * canvasElement.height };
249+
const p2 = { x: screenRightHand_norm.x * canvasElement.width, y: screenRightHand_norm.y * canvasElement.height };
250+
251+
if (showDataPoints) {
252+
drawDataPoints(p1, p2);
253+
}
254+
255+
// Draw the line and points
256+
canvasCtx.strokeStyle = '#3b82f6';
257+
canvasCtx.lineWidth = 4;
258+
canvasCtx.beginPath();
259+
canvasCtx.moveTo(p1.x, p1.y);
260+
canvasCtx.lineTo(p2.x, p2.y);
261+
canvasCtx.stroke();
262+
263+
canvasCtx.fillStyle = '#3b82f6';
264+
canvasCtx.beginPath(); canvasCtx.arc(p1.x, p1.y, 8, 0, 2 * Math.PI); canvasCtx.fill();
265+
canvasCtx.beginPath(); canvasCtx.arc(p2.x, p2.y, 8, 0, 2 * Math.PI); canvasCtx.fill();
266+
267+
// --- Coefficient Calculation using selected scales ---
268+
const scaledP1 = {
269+
x: mapRange(p1.x, 0, canvasElement.width, xScale.min, xScale.max),
270+
y: mapRange(p1.y, canvasElement.height, 0, yScale.min, yScale.max)
271+
};
272+
const scaledP2 = {
273+
x: mapRange(p2.x, 0, canvasElement.width, xScale.min, xScale.max),
274+
y: mapRange(p2.y, canvasElement.height, 0, yScale.min, yScale.max)
275+
};
276+
277+
const dx = scaledP2.x - scaledP1.x;
278+
const dy = scaledP2.y - scaledP1.y;
279+
280+
if (Math.abs(dx) < 1e-9) return;
281+
282+
const coefficient = -(dy / dx);
283+
const text = `β = ${coefficient.toFixed(2)}`;
284+
const midX = (p1.x + p2.x) / 2;
285+
const midY = (p1.y + p2.y) / 2 - 35;
286+
287+
// Draw un-mirrored text
288+
canvasCtx.save();
289+
canvasCtx.translate(midX, midY);
290+
canvasCtx.scale(-1, 1);
291+
292+
canvasCtx.font = 'bold 24px "Xanh Mono", monospace';
293+
canvasCtx.textAlign = 'center';
294+
const textMetrics = canvasCtx.measureText(text);
295+
const bgWidth = textMetrics.width + 20;
296+
const bgHeight = 40;
297+
canvasCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
298+
canvasCtx.beginPath();
299+
canvasCtx.roundRect(-bgWidth / 2, -bgHeight / 2 - 5, bgWidth, bgHeight, 8);
300+
canvasCtx.fill();
301+
canvasCtx.fillStyle = '#ffffff';
302+
canvasCtx.fillText(text, 0, 0);
303+
304+
canvasCtx.restore();
305+
}
306+
307+
// --- Helper & Initialization ---
308+
function getAveragePosition(hand, indices) {
309+
let sumX = 0, sumY = 0, count = 0;
310+
for (const index of indices) {
311+
if (hand && hand[index]) {
312+
sumX += hand[index].x;
313+
sumY += hand[index].y;
314+
count++;
315+
}
316+
}
317+
if (count === 0) return null;
318+
return { x: sumX / count, y: sumY / count };
319+
}
320+
321+
const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
322+
hands.setOptions({
323+
maxNumHands: 2, modelComplexity: 1, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5
324+
});
325+
hands.onResults(onResults);
326+
327+
const camera = new Camera(videoElement, {
328+
onFrame: async () => await hands.send({image: videoElement}),
329+
width: 640, height: 480
330+
});
331+
camera.start().then(() => {
332+
loadingContainer.style.display = 'none';
333+
container.style.display = 'block';
334+
controlsContainer.style.display = 'grid';
335+
}).catch(err => {
336+
console.error("Error starting camera:", err);
337+
loadingContainer.innerHTML = '<p class="text-red-400">Could not start camera. Please ensure you have a webcam enabled and have granted permission.</p>';
338+
});
339+
340+
</script>
341+
</body>
342+
</html>

0 commit comments

Comments
 (0)