cduss Claude Opus 4.5 commited on
Commit
8661c74
·
1 Parent(s): b0ad0e7

Replace joystick with RPY sliders synced to robot state

Browse files

- Roll, Pitch, Yaw sliders for absolute head control
- Sliders update live from robot state when not being controlled
- Removed joystick and all related code/CSS
- Cleaner, simpler interface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. index.html +74 -217
index.html CHANGED
@@ -267,97 +267,6 @@
267
  padding: 12px;
268
  }
269
 
270
- /* Joystick - Full Width Responsive */
271
- .joystick-wrapper {
272
- display: flex;
273
- align-items: center;
274
- justify-content: center;
275
- gap: 20px;
276
- padding: 10px;
277
- }
278
-
279
- .joystick-area {
280
- width: min(250px, 40vw);
281
- height: min(250px, 40vw);
282
- min-width: 180px;
283
- min-height: 180px;
284
- background: radial-gradient(circle at center, var(--pollen-card-light) 0%, var(--pollen-darker) 100%);
285
- border-radius: 50%;
286
- position: relative;
287
- border: 3px solid var(--pollen-coral);
288
- touch-action: none;
289
- cursor: grab;
290
- flex-shrink: 0;
291
- }
292
-
293
- .joystick-knob {
294
- width: 25%;
295
- height: 25%;
296
- background: var(--pollen-coral);
297
- border-radius: 50%;
298
- position: absolute;
299
- top: 50%;
300
- left: 50%;
301
- transform: translate(-50%, -50%);
302
- box-shadow: 0 4px 16px rgba(255,107,53,0.5);
303
- pointer-events: none;
304
- }
305
-
306
- .joystick-label {
307
- position: absolute;
308
- font-size: 0.7em;
309
- color: var(--text-muted);
310
- font-weight: 500;
311
- }
312
-
313
- .joystick-label.top { top: 8%; left: 50%; transform: translateX(-50%); }
314
- .joystick-label.bottom { bottom: 8%; left: 50%; transform: translateX(-50%); }
315
- .joystick-label.left { left: 5%; top: 50%; transform: translateY(-50%); }
316
- .joystick-label.right { right: 5%; top: 50%; transform: translateY(-50%); }
317
-
318
- .roll-control {
319
- display: flex;
320
- flex-direction: column;
321
- align-items: center;
322
- gap: 8px;
323
- flex-shrink: 0;
324
- }
325
-
326
- .roll-slider {
327
- writing-mode: vertical-lr;
328
- direction: rtl;
329
- height: min(220px, 38vw);
330
- min-height: 160px;
331
- width: 16px;
332
- -webkit-appearance: none;
333
- background: var(--pollen-darker);
334
- border-radius: 8px;
335
- border: 2px solid var(--pollen-card-light);
336
- }
337
-
338
- .roll-slider::-webkit-slider-thumb {
339
- -webkit-appearance: none;
340
- width: 36px;
341
- height: 36px;
342
- background: var(--pollen-coral);
343
- border-radius: 50%;
344
- cursor: pointer;
345
- box-shadow: 0 2px 10px rgba(255,107,53,0.5);
346
- }
347
-
348
- .roll-label {
349
- font-size: 0.75em;
350
- color: var(--text-muted);
351
- font-weight: 500;
352
- }
353
-
354
- .joystick-hint {
355
- text-align: center;
356
- font-size: 0.75em;
357
- color: var(--text-muted);
358
- padding-top: 8px;
359
- }
360
-
361
  /* Sliders */
362
  .slider-row {
363
  display: flex;
@@ -578,15 +487,6 @@
578
  grid-column: 2;
579
  grid-row: 4;
580
  }
581
-
582
- .joystick-area {
583
- width: 220px;
584
- height: 220px;
585
- }
586
-
587
- .roll-slider {
588
- height: 200px;
589
- }
590
  }
591
 
592
  /* Login View */
@@ -716,20 +616,25 @@
716
  </div>
717
  </div>
718
 
719
- <!-- Joystick Control -->
720
  <div class="panel">
721
- <div class="panel-header">Head Control</div>
722
  <div class="panel-content">
723
- <div class="joystick-wrapper">
724
- <div class="joystick-area" id="joystick">
725
- <div class="joystick-knob" id="joystickKnob"></div>
726
- <span class="joystick-label top">Up</span>
727
- <span class="joystick-label bottom">Down</span>
728
- <span class="joystick-label left">Left</span>
729
- <span class="joystick-label right">Right</span>
730
- </div>
 
 
 
 
 
 
731
  </div>
732
- <div class="joystick-hint">Hold and drag to move head continuously</div>
733
  </div>
734
  </div>
735
 
@@ -804,14 +709,8 @@
804
  let sseAbortController = null;
805
  let stateRefreshInterval = null;
806
 
807
- // Joystick state - track our own target position
808
- let targetYaw = 0;
809
- let targetPitch = 0;
810
- let targetRoll = 0;
811
- let joystickActive = false;
812
- let joystickX = 0;
813
- let joystickY = 0;
814
- let joystickInterval = null;
815
 
816
  // Audio state
817
  let localStream = null;
@@ -834,7 +733,7 @@
834
 
835
  document.addEventListener('DOMContentLoaded', () => {
836
  initAuth();
837
- initJoystick();
838
  initAntennaSliders();
839
  });
840
 
@@ -1084,7 +983,6 @@
1084
 
1085
  function stopStream() {
1086
  if (stateRefreshInterval) clearInterval(stateRefreshInterval);
1087
- if (joystickInterval) clearInterval(joystickInterval);
1088
  if (peerConnection) peerConnection.close();
1089
  if (dataChannel) dataChannel.close();
1090
  peerConnection = null;
@@ -1118,15 +1016,20 @@
1118
  const pitch = Math.asin(-m[2][0]) * 180 / Math.PI;
1119
  const yaw = Math.atan2(m[1][0], m[0][0]) * 180 / Math.PI;
1120
  const roll = Math.atan2(m[2][1], m[2][2]) * 180 / Math.PI;
 
 
1121
  document.getElementById('stateYaw').textContent = yaw.toFixed(1) + '°';
1122
  document.getElementById('statePitch').textContent = pitch.toFixed(1) + '°';
1123
  document.getElementById('stateRoll').textContent = roll.toFixed(1) + '°';
1124
 
1125
- // Sync target with robot state when not using joystick
1126
- if (!joystickActive) {
1127
- targetYaw = yaw;
1128
- targetPitch = pitch;
1129
- targetRoll = roll;
 
 
 
1130
  }
1131
  }
1132
  if (state.body_yaw !== undefined) {
@@ -1147,105 +1050,59 @@
1147
  }
1148
  }
1149
 
1150
- // ===================== Joystick =====================
1151
- function initJoystick() {
1152
- const joystick = document.getElementById('joystick');
1153
- const knob = document.getElementById('joystickKnob');
1154
-
1155
- function getPosition(e) {
1156
- const rect = joystick.getBoundingClientRect();
1157
- const size = rect.width;
1158
- const centerX = size / 2;
1159
- const centerY = size / 2;
1160
- const touch = e.touches ? e.touches[0] : e;
1161
- let x = touch.clientX - rect.left - centerX;
1162
- let y = touch.clientY - rect.top - centerY;
1163
- const maxRadius = centerX * 0.7;
1164
- const dist = Math.sqrt(x * x + y * y);
1165
- if (dist > maxRadius) {
1166
- x = (x / dist) * maxRadius;
1167
- y = (y / dist) * maxRadius;
1168
- }
1169
- return {
1170
- x, y,
1171
- normX: x / maxRadius, // -1 to 1 (left to right)
1172
- normY: y / maxRadius // -1 to 1 (top to bottom)
1173
- };
1174
- }
1175
-
1176
- function updateKnob(pos) {
1177
- const rect = joystick.getBoundingClientRect();
1178
- knob.style.left = (rect.width / 2 + pos.x) + 'px';
1179
- knob.style.top = (rect.height / 2 + pos.y) + 'px';
1180
- }
1181
-
1182
- function startJoystick(e) {
1183
- e.preventDefault();
1184
- joystickActive = true;
1185
- const pos = getPosition(e);
1186
- updateKnob(pos);
1187
- joystickX = pos.normX;
1188
- joystickY = pos.normY;
1189
- startContinuousMovement();
1190
  }
1191
 
1192
- function moveJoystick(e) {
1193
- if (!joystickActive) return;
1194
- e.preventDefault();
1195
- const pos = getPosition(e);
1196
- updateKnob(pos);
1197
- joystickX = pos.normX;
1198
- joystickY = pos.normY;
1199
  }
1200
 
1201
- function endJoystick() {
1202
- joystickActive = false;
1203
- knob.style.left = '50%';
1204
- knob.style.top = '50%';
1205
- joystickX = 0;
1206
- joystickY = 0;
1207
- stopContinuousMovement();
1208
  }
1209
 
1210
- joystick.addEventListener('mousedown', startJoystick);
1211
- joystick.addEventListener('touchstart', startJoystick, { passive: false });
1212
- document.addEventListener('mousemove', moveJoystick);
1213
- document.addEventListener('touchmove', moveJoystick, { passive: false });
1214
- document.addEventListener('mouseup', endJoystick);
1215
- document.addEventListener('touchend', endJoystick);
1216
-
1217
- }
1218
-
1219
- function startContinuousMovement() {
1220
- if (joystickInterval) return;
1221
-
1222
- joystickInterval = setInterval(() => {
1223
- if (!joystickActive) return;
1224
-
1225
- const speed = 1.5; // degrees per tick
1226
-
1227
- // Joystick mapping:
1228
- // Left/Right (X) controls Yaw: right = turn right (negative yaw)
1229
- // Up/Down (Y) controls Pitch: up = look up (negative pitch)
1230
- targetYaw += -joystickX * speed;
1231
- targetPitch += joystickY * speed;
1232
-
1233
- // Clamp to limits
1234
- targetYaw = Math.max(-45, Math.min(45, targetYaw));
1235
- targetPitch = Math.max(-30, Math.min(30, targetPitch));
1236
- targetRoll = Math.max(-20, Math.min(20, targetRoll));
1237
-
1238
- // Send command
1239
- sendCommand({ set_target: buildMatrix(targetYaw, targetPitch, targetRoll) });
1240
 
1241
- }, 50);
1242
- }
 
 
 
 
 
 
 
1243
 
1244
- function stopContinuousMovement() {
1245
- if (joystickInterval) {
1246
- clearInterval(joystickInterval);
1247
- joystickInterval = null;
1248
- }
 
 
 
 
1249
  }
1250
 
1251
  function buildMatrix(yawDeg, pitchDeg, rollDeg) {
 
267
  padding: 12px;
268
  }
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  /* Sliders */
271
  .slider-row {
272
  display: flex;
 
487
  grid-column: 2;
488
  grid-row: 4;
489
  }
 
 
 
 
 
 
 
 
 
490
  }
491
 
492
  /* Login View */
 
616
  </div>
617
  </div>
618
 
619
+ <!-- Head Control - RPY Sliders -->
620
  <div class="panel">
621
+ <div class="panel-header">Head Orientation</div>
622
  <div class="panel-content">
623
+ <div class="slider-row">
624
+ <span class="slider-label">Roll</span>
625
+ <input type="range" class="slider" id="rollSlider" min="-20" max="20" value="0" step="0.5">
626
+ <span class="slider-value" id="rollValue">0.0°</span>
627
+ </div>
628
+ <div class="slider-row">
629
+ <span class="slider-label">Pitch</span>
630
+ <input type="range" class="slider" id="pitchSlider" min="-30" max="30" value="0" step="0.5">
631
+ <span class="slider-value" id="pitchValue">0.0°</span>
632
+ </div>
633
+ <div class="slider-row">
634
+ <span class="slider-label">Yaw</span>
635
+ <input type="range" class="slider" id="yawSlider" min="-45" max="45" value="0" step="0.5">
636
+ <span class="slider-value" id="yawValue">0.0°</span>
637
  </div>
 
638
  </div>
639
  </div>
640
 
 
709
  let sseAbortController = null;
710
  let stateRefreshInterval = null;
711
 
712
+ // Head control state
713
+ let headSlidersActive = false; // True while user is dragging a slider
 
 
 
 
 
 
714
 
715
  // Audio state
716
  let localStream = null;
 
733
 
734
  document.addEventListener('DOMContentLoaded', () => {
735
  initAuth();
736
+ initHeadSliders();
737
  initAntennaSliders();
738
  });
739
 
 
983
 
984
  function stopStream() {
985
  if (stateRefreshInterval) clearInterval(stateRefreshInterval);
 
986
  if (peerConnection) peerConnection.close();
987
  if (dataChannel) dataChannel.close();
988
  peerConnection = null;
 
1016
  const pitch = Math.asin(-m[2][0]) * 180 / Math.PI;
1017
  const yaw = Math.atan2(m[1][0], m[0][0]) * 180 / Math.PI;
1018
  const roll = Math.atan2(m[2][1], m[2][2]) * 180 / Math.PI;
1019
+
1020
+ // Update state bar
1021
  document.getElementById('stateYaw').textContent = yaw.toFixed(1) + '°';
1022
  document.getElementById('statePitch').textContent = pitch.toFixed(1) + '°';
1023
  document.getElementById('stateRoll').textContent = roll.toFixed(1) + '°';
1024
 
1025
+ // Update sliders with real position when not being controlled
1026
+ if (!headSlidersActive) {
1027
+ document.getElementById('rollSlider').value = roll;
1028
+ document.getElementById('rollValue').textContent = roll.toFixed(1) + '°';
1029
+ document.getElementById('pitchSlider').value = pitch;
1030
+ document.getElementById('pitchValue').textContent = pitch.toFixed(1) + '°';
1031
+ document.getElementById('yawSlider').value = yaw;
1032
+ document.getElementById('yawValue').textContent = yaw.toFixed(1) + '°';
1033
  }
1034
  }
1035
  if (state.body_yaw !== undefined) {
 
1050
  }
1051
  }
1052
 
1053
+ // ===================== Head Sliders =====================
1054
+ function initHeadSliders() {
1055
+ const rollSlider = document.getElementById('rollSlider');
1056
+ const pitchSlider = document.getElementById('pitchSlider');
1057
+ const yawSlider = document.getElementById('yawSlider');
1058
+ const rollValue = document.getElementById('rollValue');
1059
+ const pitchValue = document.getElementById('pitchValue');
1060
+ const yawValue = document.getElementById('yawValue');
1061
+
1062
+ function sendHeadPose() {
1063
+ const roll = parseFloat(rollSlider.value);
1064
+ const pitch = parseFloat(pitchSlider.value);
1065
+ const yaw = parseFloat(yawSlider.value);
1066
+ sendCommand({ set_target: buildMatrix(yaw, pitch, roll) });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1067
  }
1068
 
1069
+ function onSliderStart() {
1070
+ headSlidersActive = true;
 
 
 
 
 
1071
  }
1072
 
1073
+ function onSliderEnd() {
1074
+ headSlidersActive = false;
 
 
 
 
 
1075
  }
1076
 
1077
+ // Roll slider
1078
+ rollSlider.addEventListener('mousedown', onSliderStart);
1079
+ rollSlider.addEventListener('touchstart', onSliderStart);
1080
+ rollSlider.addEventListener('mouseup', onSliderEnd);
1081
+ rollSlider.addEventListener('touchend', onSliderEnd);
1082
+ rollSlider.addEventListener('input', () => {
1083
+ rollValue.textContent = parseFloat(rollSlider.value).toFixed(1) + '°';
1084
+ sendHeadPose();
1085
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
 
1087
+ // Pitch slider
1088
+ pitchSlider.addEventListener('mousedown', onSliderStart);
1089
+ pitchSlider.addEventListener('touchstart', onSliderStart);
1090
+ pitchSlider.addEventListener('mouseup', onSliderEnd);
1091
+ pitchSlider.addEventListener('touchend', onSliderEnd);
1092
+ pitchSlider.addEventListener('input', () => {
1093
+ pitchValue.textContent = parseFloat(pitchSlider.value).toFixed(1) + '°';
1094
+ sendHeadPose();
1095
+ });
1096
 
1097
+ // Yaw slider
1098
+ yawSlider.addEventListener('mousedown', onSliderStart);
1099
+ yawSlider.addEventListener('touchstart', onSliderStart);
1100
+ yawSlider.addEventListener('mouseup', onSliderEnd);
1101
+ yawSlider.addEventListener('touchend', onSliderEnd);
1102
+ yawSlider.addEventListener('input', () => {
1103
+ yawValue.textContent = parseFloat(yawSlider.value).toFixed(1) + '°';
1104
+ sendHeadPose();
1105
+ });
1106
  }
1107
 
1108
  function buildMatrix(yawDeg, pitchDeg, rollDeg) {