UI improvements, and control softening
This commit is contained in:
@@ -90,10 +90,10 @@ function setHeave(v,send=true){if(!canDrive()&&v!==0)return;v=Math.max(-1,Math.m
|
||||
function heaveEvent(e){const b=heave.getBoundingClientRect();setHeave((b.top+b.height/2-e.clientY)/(b.height*.43))}
|
||||
function releaseHeave(e){if(heavePointer===null||(e?.pointerId!=null&&e.pointerId!==heavePointer))return;const pointer=heavePointer;heavePointer=null;try{heave.releasePointerCapture(pointer)}catch(_){}releaseHandle(hknob);easeHeave()}
|
||||
heave.addEventListener('pointerdown',e=>{if(!canDrive()||heavePointer!==null||e.button>0)return;heavePointer=e.pointerId;hknob.classList.remove('released');hknob.classList.add('pressed');cancelAnimationFrame(heaveAnim);heave.setPointerCapture(e.pointerId);heaveEvent(e);e.preventDefault()});heave.addEventListener('pointermove',e=>{if(e.pointerId===heavePointer)heaveEvent(e)});heave.addEventListener('pointerup',releaseHeave);heave.addEventListener('pointercancel',releaseHeave);heave.addEventListener('lostpointercapture',e=>{if(e.pointerId===heavePointer)releaseHeave(e)});
|
||||
function easeHeave(){const sv=control.vertical,start=performance.now();function frame(now){const p=Math.min(1,(now-start)/400),smooth=p*p*(3-2*p);setHeave(sv*(1-smooth));if(p<1)heaveAnim=requestAnimationFrame(frame)}heaveAnim=requestAnimationFrame(frame)}
|
||||
function easeHeave(target=0){cancelAnimationFrame(heaveAnim);const sv=control.vertical,start=performance.now();function frame(now){const p=Math.min(1,(now-start)/400),smooth=p*p*(3-2*p);setHeave(sv+(target-sv)*smooth);if(p<1)heaveAnim=requestAnimationFrame(frame)}heaveAnim=requestAnimationFrame(frame)}
|
||||
window.verticalControl={setPos:value=>{cancelAnimationFrame(heaveAnim);setHeave(value)},easeBack:()=>{if(heavePointer===null)easeHeave()},getState:()=>({value:control.vertical,active:heavePointer!==null})};
|
||||
|
||||
function applyKeys(){if(!canDrive()){pressedKeys.clear();return;}const turn=(pressedKeys.has('d')||pressedKeys.has('e')?1:0)-(pressedKeys.has('a')||pressedKeys.has('q')?1:0),forward=(pressedKeys.has('w')?1:0)-(pressedKeys.has('s')?1:0),vertical=(pressedKeys.has('r')?1:0)-(pressedKeys.has('f')?1:0);if(!window.joystick3D?.getState().active){if(turn||forward)window.joystick3D?.setPos(turn,-forward);else window.joystick3D?.easeBack()}if(heavePointer===null){if(vertical){cancelAnimationFrame(heaveAnim);setHeave(vertical)}else easeHeave()}}
|
||||
function applyKeys(){if(!canDrive()){pressedKeys.clear();return;}const turn=(pressedKeys.has('d')||pressedKeys.has('e')?1:0)-(pressedKeys.has('a')||pressedKeys.has('q')?1:0),forward=(pressedKeys.has('w')?1:0)-(pressedKeys.has('s')?1:0),vertical=(pressedKeys.has('r')?1:0)-(pressedKeys.has('f')?1:0);if(!window.joystick3D?.getState().active)window.joystick3D?.easeTo(turn,-forward);if(heavePointer===null)easeHeave(vertical)}
|
||||
addEventListener('keydown',e=>{if(e.key==='F3'){e.preventDefault();if(!e.repeat&&view.action==='disarm')requestControl('disarm');return;}if(/^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName))return;if(e.key==='F2'){e.preventDefault();if(!e.repeat)startEngageHold();return;}const k=e.key.toLowerCase();if('wasdqerf'.includes(k)&&k.length===1){if(!canDrive())return;e.preventDefault();if(!pressedKeys.has(k)){pressedKeys.add(k);applyKeys();}}});addEventListener('keyup',e=>{if(e.key==='F2'){e.preventDefault();cancelEngageHold(true);return;}const k=e.key.toLowerCase();if(pressedKeys.delete(k))applyKeys();});
|
||||
function resetInputs(){
|
||||
if(gainPointer!==null)releaseGain({pointerId:gainPointer});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/* Decorative only: no control demands or vehicle state are accessed here. */
|
||||
(() => {
|
||||
const workspace=document.querySelector('.controls-workspace');
|
||||
if(!workspace)return;
|
||||
const wash=document.createElement('span');
|
||||
wash.className='workspace-light';wash.setAttribute('aria-hidden','true');workspace.append(wash);
|
||||
// Explicit visual surfaces, not every semantic button. The entire sidebar
|
||||
// and text-only Engage control are excluded from local lighting.
|
||||
const pillSelector='.config-btn,.modal .button,.range-pill,.number-pill';
|
||||
const shapeSelector=`${pillSelector},.heave-track,.heave-knob,.joystick,.modal`;
|
||||
const layers=[...document.querySelectorAll(shapeSelector)].filter(host=>!host.closest('.sidebar-deck,#engage')).map(host=>{
|
||||
host.classList.add('light-surface');
|
||||
const layer=document.createElement('span');layer.className='cursor-light';
|
||||
layer.setAttribute('aria-hidden','true');
|
||||
layer.classList.toggle('pill-light',host.matches(pillSelector));
|
||||
host.append(layer);return {host,layer};
|
||||
});
|
||||
let frame=0,point=null;
|
||||
const clear=()=>{point=null;cancelAnimationFrame(frame);frame=0;wash.classList.remove('lit');layers.forEach(({layer})=>layer.classList.remove('lit','near'))};
|
||||
function paint(){
|
||||
frame=0;if(!point)return;
|
||||
const {x,y}=point,r=workspace.getBoundingClientRect();
|
||||
wash.style.setProperty('--light-x',`${x-r.left}px`);wash.style.setProperty('--light-y',`${y-r.top}px`);
|
||||
wash.classList.toggle('lit',x>=r.left&&x<=r.right&&y>=r.top&&y<=r.bottom);
|
||||
for(const {host,layer} of layers){
|
||||
const b=host.getBoundingClientRect();
|
||||
layer.style.setProperty('--light-x',`${x-b.left}px`);layer.style.setProperty('--light-y',`${y-b.top}px`);
|
||||
const inside=x>=b.left&&x<=b.right&&y>=b.top&&y<=b.bottom;
|
||||
// CSS gradient angles start at up; atan2 is measured from the x axis.
|
||||
const angle=Math.atan2(y-(b.top+b.height/2),x-(b.left+b.width/2))*180/Math.PI+90;
|
||||
layer.style.setProperty('--light-angle',`${angle}deg`);
|
||||
layer.classList.toggle('lit',inside);layer.classList.add('near');
|
||||
}
|
||||
}
|
||||
document.addEventListener('pointermove',event=>{
|
||||
if(event.pointerType==='touch'){clear();return}
|
||||
point={x:event.clientX,y:event.clientY};if(!frame)frame=requestAnimationFrame(paint);
|
||||
},{passive:true});
|
||||
document.documentElement.addEventListener('pointerleave',clear);
|
||||
document.addEventListener('pointercancel',clear);window.addEventListener('blur',clear);
|
||||
document.addEventListener('visibilitychange',()=>{if(document.hidden)clear()});
|
||||
})();
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Sub Control</title>
|
||||
<link rel="stylesheet" href="styles.css?v=20260827-bridge-v5">
|
||||
<link rel="stylesheet" href="styles.css?v=20260827-model-layout-v14">
|
||||
</head>
|
||||
<body>
|
||||
<main class="app-shell">
|
||||
@@ -132,9 +132,9 @@
|
||||
<label class="output-limit">
|
||||
<span>Output limit <b id="gain-value">20%</b></span>
|
||||
<div class="range-pill">
|
||||
<button id="gain-down" type="button" aria-label="Decrease output limit">‹</button>
|
||||
<button id="gain-down" type="button" aria-label="Decrease output limit"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m14 6-6 6 6 6"/></svg></button>
|
||||
<input id="gain" aria-label="Output limit" type="range" min="5" max="50" value="20">
|
||||
<button id="gain-up" type="button" aria-label="Increase output limit">›</button>
|
||||
<button id="gain-up" type="button" aria-label="Increase output limit"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m10 6 6 6-6 6"/></svg></button>
|
||||
</div>
|
||||
</label>
|
||||
<button class="button subtle config-btn" data-open="motors-modal">Motor setup</button>
|
||||
@@ -151,8 +151,9 @@
|
||||
<dialog id="motors-modal" class="modal motor-modal"><button class="modal-close" data-close>×</button><small>configuration</small><h2>Motor Outputs</h2><p class="note">Function and direction are read one parameter at a time to protect the low-bandwidth link.</p><div class="motor-head"><span>Output</span><span>Function</span><span>Direction</span><span></span></div><div id="motor-list" class="motor-list"></div></dialog>
|
||||
|
||||
<script src="model-viewer.js?v=20260826-controls-v2"></script>
|
||||
<script src="joystick-3d.js?v=20260827-polish-v3"></script>
|
||||
<script src="joystick-3d.js?v=20260827-keyboard-v7"></script>
|
||||
<script src="control-state.js?v=20260827-state-v1"></script>
|
||||
<script src="app.js?v=20260827-bridge-v6"></script>
|
||||
<script src="app.js?v=20260827-keyboard-v7"></script>
|
||||
<script src="cursor-light.js?v=20260827-directional-v10"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -30,7 +30,14 @@
|
||||
function publish(){window.setJoyControl?.(x,y)}
|
||||
function setPosition(nextX,nextY,notify=true){const length=Math.hypot(nextX,nextY);if(length>1){nextX/=length;nextY/=length}x=nextX;y=nextY;draw();if(notify)publish()}
|
||||
function positionFromEvent(event){const rect=canvas.getBoundingClientRect(),g=geometry();setPosition((event.clientX-rect.left-g.cx)/g.travel,(event.clientY-rect.top-g.cy)/g.travel)}
|
||||
function returnToCenter(){cancelAnimationFrame(returnFrame);const startX=x,startY=y,start=performance.now();const tick=now=>{const p=Math.min(1,(now-start)/360),smooth=p*p*(3-2*p);setPosition(startX*(1-smooth),startY*(1-smooth));if(p<1)returnFrame=requestAnimationFrame(tick)};returnFrame=requestAnimationFrame(tick)}
|
||||
function easeTo(nextX,nextY){
|
||||
cancelAnimationFrame(returnFrame);
|
||||
const length=Math.hypot(nextX,nextY);if(length>1){nextX/=length;nextY/=length}
|
||||
const startX=x,startY=y,start=performance.now();
|
||||
const tick=now=>{const p=Math.min(1,(now-start)/360),smooth=p*p*(3-2*p);setPosition(startX+(nextX-startX)*smooth,startY+(nextY-startY)*smooth);if(p<1)returnFrame=requestAnimationFrame(tick)};
|
||||
returnFrame=requestAnimationFrame(tick);
|
||||
}
|
||||
function returnToCenter(){easeTo(0,0)}
|
||||
function release(event){if(activePointer===null||(event?.pointerId!=null&&event.pointerId!==activePointer))return;const pointer=activePointer;activePointer=null;try{canvas.releasePointerCapture(pointer)}catch(_){}canvas.classList.remove('is-active');draw();returnToCenter()}
|
||||
|
||||
canvas.addEventListener('pointerdown',event=>{if(activePointer!==null||!window.canDrive?.()||event.button>0)return;activePointer=event.pointerId;cancelAnimationFrame(returnFrame);canvas.setPointerCapture(event.pointerId);canvas.classList.add('is-active');positionFromEvent(event);event.preventDefault()});
|
||||
@@ -38,5 +45,5 @@
|
||||
canvas.addEventListener('pointerup',release);canvas.addEventListener('pointercancel',release);canvas.addEventListener('lostpointercapture',event=>{if(event.pointerId===activePointer)release(event)});
|
||||
canvas.addEventListener('pointerleave',event=>{if(event.pointerType==='mouse'&&event.pointerId===activePointer)release(event)});
|
||||
addEventListener('resize',resize);resize();
|
||||
window.joystick3D={setPos(nextX,nextY){cancelAnimationFrame(returnFrame);setPosition(nextX,nextY)},easeBack(){if(activePointer===null)returnToCenter()},reset(){const pointer=activePointer;activePointer=null;cancelAnimationFrame(returnFrame);if(pointer!==null)try{canvas.releasePointerCapture(pointer)}catch(_){}canvas.classList.remove('is-active');setPosition(0,0,false)},getState(){return {x,y,active:activePointer!==null}}};
|
||||
window.joystick3D={setPos(nextX,nextY){cancelAnimationFrame(returnFrame);setPosition(nextX,nextY)},easeTo(nextX,nextY){if(activePointer===null)easeTo(nextX,nextY)},easeBack(){if(activePointer===null)returnToCenter()},reset(){const pointer=activePointer;activePointer=null;cancelAnimationFrame(returnFrame);if(pointer!==null)try{canvas.releasePointerCapture(pointer)}catch(_){}canvas.classList.remove('is-active');setPosition(0,0,false)},getState(){return {x,y,active:activePointer!==null}}};
|
||||
})();
|
||||
|
||||
+26
-10
@@ -1,5 +1,19 @@
|
||||
/* SUB Control UI – Desktop Layout */
|
||||
|
||||
/* Isolated pointer lighting: no layout or input interception. */
|
||||
.controls-workspace { position:relative; isolation:isolate; }
|
||||
.workspace-light { position:absolute; inset:0; z-index:-1; pointer-events:none; border-radius:inherit; opacity:0; background:radial-gradient(600px circle at var(--light-x) var(--light-y),rgba(255,255,255,.35),transparent 75%); transition:opacity .35s ease-out; }
|
||||
.workspace-light.lit { opacity:1; }
|
||||
.light-surface { position:relative; }
|
||||
.cursor-light { position:absolute; inset:0; z-index:3; pointer-events:none; border-radius:inherit; }
|
||||
.cursor-light::before,.cursor-light::after { content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none; opacity:0; transition:opacity .35s ease-out; }
|
||||
.cursor-light.pill-light::before { background:radial-gradient(240px circle at var(--light-x) var(--light-y),rgba(110,190,235,.08),transparent 80%); }
|
||||
.cursor-light::after { padding:1px; background:linear-gradient(var(--light-angle,315deg),transparent 30%,rgba(255,255,255,.13) 65%,rgba(255,255,255,.50) 100%); -webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0); -webkit-mask-composite:xor; mask-composite:exclude; }
|
||||
.cursor-light.lit::before,.cursor-light.near::after { opacity:1; }
|
||||
.joystick > .cursor-light { inset:7%; border-radius:50%; }
|
||||
#engage > .cursor-light::after { display:none; }
|
||||
@media (prefers-reduced-motion:reduce) { .workspace-light,.cursor-light::before,.cursor-light::after { transition:none; } }
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0c1012;
|
||||
@@ -14,7 +28,7 @@
|
||||
--amber: #f2c675;
|
||||
--red: #f0958b;
|
||||
--shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
|
||||
font-family: Inter, "SF Pro Display", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@@ -1039,7 +1053,7 @@ body #connect, body #connect:hover, body #connect.is-connected, body #connect.is
|
||||
}
|
||||
body #connect .btn-label { animation:none; }
|
||||
.telemetry-zone { gap:20px; }
|
||||
.rov-stage { height:clamp(180px,32vh,250px); min-height:180px; touch-action:none; }
|
||||
.rov-stage { flex:1; height:auto; min-height:0; width:100%; touch-action:none; border-radius:31px; overflow:hidden; background:transparent; }
|
||||
.model-card { justify-content:flex-start; }
|
||||
.rov-stage::before { display:none; }
|
||||
.model-tools button { font-size:12px; padding:7px 11px; }
|
||||
@@ -1052,24 +1066,25 @@ body #connect .btn-label { animation:none; }
|
||||
.activity-history li { font-size:12px; font-weight:300; line-height:1.25; color:#999; overflow-wrap:anywhere; }
|
||||
.activity-history #status-text { color:#fff; font-size:12px; font-weight:300; }
|
||||
.activity-history li[hidden] { display:none; }
|
||||
.controls-workspace { padding:8px 20px 16px; background:#f6f6f6; }
|
||||
.controls-workspace { padding:8px 20px 16px; background:#ededed; }
|
||||
.primary-control-zone { grid-template-rows:120px minmax(0,1fr) 52px; }
|
||||
.engage-header-zone { display:block; position:relative; height:120px; min-height:0; margin:0; padding:0; }
|
||||
body #engage { width:calc(100% + 40px); margin-left:-20px; height:104px; padding:0 20px; border:0; border-radius:0; background:transparent; box-shadow:none; }
|
||||
body #engage:hover, body #engage.pending, body #engage.danger { background:transparent; border:0; }
|
||||
#engage::before, #engage::after { display:none; }
|
||||
body #engage { align-items:flex-end; padding-bottom:12px; }
|
||||
.engage-face > span { font-family:"Segoe UI Variable Display","Segoe UI",Arial,sans-serif; font-size:clamp(64px,7.3vw,90px); font-weight:200; letter-spacing:.015em; line-height:1; padding-bottom:.22em; margin-bottom:-.22em; }
|
||||
.engage-face > span { font-family:inherit; font-size:clamp(64px,7.3vw,90px); font-weight:200; letter-spacing:.015em; line-height:1; padding-bottom:.22em; margin-bottom:-.22em; }
|
||||
#engage { --engage-green:#4cce73; --engage-glow:drop-shadow(0 0 2px rgba(76,206,115,.3)); }
|
||||
#engage .engage-label { color:#777; background:none; animation:none; }
|
||||
.engage-sheen-label { color:#fff; pointer-events:none; opacity:0; mask-image:linear-gradient(105deg,transparent 40%,#000 49%,#000 51%,transparent 60%); mask-size:300% 100%; mask-repeat:no-repeat; -webkit-mask-image:linear-gradient(105deg,transparent 40%,#000 49%,#000 51%,transparent 60%); -webkit-mask-size:300% 100%; -webkit-mask-repeat:no-repeat; }
|
||||
#engage:is(.arm,.danger):not(:disabled):not(.charging):not(.canceling):not(.state-transition) .engage-sheen-label { opacity:1; animation:engage-sweep 2.2s linear infinite; }
|
||||
@keyframes engage-sweep { from {mask-position:100% 0;-webkit-mask-position:100% 0} to {mask-position:0% 0;-webkit-mask-position:0% 0} }
|
||||
.engage-progress-label { color:#4cce73; clip-path:none; filter:drop-shadow(0 0 2px rgba(76,206,115,.3)); mask-image:linear-gradient(to right,#000 calc(var(--engage-progress) - 8%),transparent var(--engage-progress)); -webkit-mask-image:linear-gradient(to right,#000 calc(var(--engage-progress) - 8%),transparent var(--engage-progress)); }
|
||||
.engage-progress-label { color:var(--engage-green); clip-path:none; filter:var(--engage-glow); mask-image:linear-gradient(to right,#000 calc(var(--engage-progress) - 8%),transparent var(--engage-progress)); -webkit-mask-image:linear-gradient(to right,#000 calc(var(--engage-progress) - 8%),transparent var(--engage-progress)); }
|
||||
#engage.charged .engage-progress-label { mask-image:none; -webkit-mask-image:none; }
|
||||
#engage.pending .engage-label { color:#5a8753; background:none; }
|
||||
#engage.danger .engage-label { color:#171717; }
|
||||
#engage.pending .engage-label { color:var(--engage-green); filter:var(--engage-glow); background:none; }
|
||||
#engage.danger .engage-label { color:#777; }
|
||||
#engage:focus-visible { outline:2px solid #8d9991; outline-offset:-6px; }
|
||||
.control-status { position:absolute; z-index:2; top:100%; left:0; right:0; margin:0; min-height:0; text-align:center; font-size:12px; max-height:48px; overflow:auto; background:#f6f6f6; }
|
||||
.control-status { position:absolute; z-index:2; top:100%; left:0; right:0; margin:0; min-height:0; text-align:center; font-size:12px; max-height:48px; overflow:auto; background:transparent; }
|
||||
.control-status[hidden] { display:none; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.engage-sheen-label { display:none; }
|
||||
@@ -1094,10 +1109,11 @@ body #engage { align-items:flex-end; padding-bottom:12px; }
|
||||
.heave-fill { left:22px; right:22px; background:#999; }
|
||||
.control-toolbar { display:flex; justify-content:center; align-items:center; gap:16px; padding-top:12px; }
|
||||
.control-toolbar .output-limit { width:auto; flex-direction:row; align-items:center; font-size:13px; gap:12px; color:#777; }
|
||||
.control-toolbar .output-limit span { flex:0 0 128px; width:128px; gap:8px; align-items:baseline; justify-content:space-between; }
|
||||
.control-toolbar .output-limit > span { flex:0 0 128px; width:128px; gap:8px; align-items:baseline; justify-content:space-between; }
|
||||
.control-toolbar .output-limit b { display:inline-block; flex:0 0 4ch; width:4ch; text-align:right; color:#333; font-variant-numeric:tabular-nums; }
|
||||
.control-toolbar .range-pill { width:175px; grid-template-columns:28px 1fr 28px; height:40px; background:#e7e7e7; border:0; }
|
||||
.control-toolbar .range-pill button { width:28px; height:40px; color:#666; font-size:22px; }
|
||||
.control-toolbar .range-pill button { width:28px; height:40px; padding:0; display:grid; place-items:center; color:#666; line-height:1; }
|
||||
.control-toolbar .range-pill button svg { width:24px; height:24px; display:block; }
|
||||
.control-toolbar .range-pill input[type="range"] { background:#c4c4c4; height:20px; background-clip:content-box; padding:8px 0; }
|
||||
.control-toolbar .range-pill input[type="range"]::-webkit-slider-thumb { width:18px; height:18px; background:#858585; }
|
||||
.control-toolbar .config-btn { width:auto; height:40px; padding:0 18px; font-size:13px; background:#e7e7e7; color:#444; border:0; border-radius:999px; }
|
||||
|
||||
Reference in New Issue
Block a user