549 lines
20 KiB
HTML
549 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>控制器</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;background-color:#fff;font-family:'Segoe UI',Arial,sans-serif;padding:20px}
|
|
.page-title{color:#333;margin-bottom:20px;text-align:center;font-weight:600;font-size:1.5rem}
|
|
|
|
/* 摇杆容器 - 关键修改:使用flex-wrap并设置固定间距 */
|
|
.joysticks-container{
|
|
display:flex;
|
|
gap:15px; /* 摇杆之间的间距 */
|
|
align-items:center;
|
|
justify-content:center;
|
|
flex-wrap:wrap; /* 允许换行 */
|
|
width:100%;
|
|
max-width:1200px;
|
|
padding:10px 0;
|
|
}
|
|
|
|
/* 滚动条美化 */
|
|
.joysticks-container::-webkit-scrollbar{height:6px;}
|
|
.joysticks-container::-webkit-scrollbar-thumb{background-color:#4a90e2;border-radius:3px;}
|
|
.joysticks-container::-webkit-scrollbar-track{background-color:#f1f1f1;border-radius:3px;}
|
|
|
|
/* 摇杆项 - 关键修改:固定为50%宽度(减去间距) */
|
|
.joystick-item{
|
|
display:flex;
|
|
flex-direction:column;
|
|
align-items:center;
|
|
gap:10px;
|
|
width:calc(50% - 10px); /* 50%宽度减去一半间距 */
|
|
min-width:unset; /* 移除最小宽度限制 */
|
|
padding:0 5px;
|
|
}
|
|
|
|
.joystick-label{
|
|
color:#333;
|
|
font-weight:500;
|
|
font-size:14px;
|
|
text-align:center;
|
|
padding:3px 8px;
|
|
border-radius:4px;
|
|
background-color:rgba(255,255,255,.8);
|
|
white-space:nowrap;
|
|
}
|
|
|
|
.joystick-wrapper{
|
|
display:flex;
|
|
position:relative;
|
|
width:100%; /* 占满父容器宽度 */
|
|
justify-content:center;
|
|
}
|
|
|
|
.joystick-wrapper.vertical{flex-direction:column;align-items:center;}
|
|
.joystick-wrapper.horizontal{flex-direction:row;align-items:center;}
|
|
|
|
.joystick-track{
|
|
position:relative;
|
|
border:2px solid #4a90e2;
|
|
border-radius:30px;
|
|
overflow:hidden;
|
|
}
|
|
|
|
.joystick-track:hover{box-shadow:0 6px 16px rgba(0,0,0,.15);}
|
|
|
|
/* 轨道尺寸 - 水平轨道占满50%屏幕宽度,垂直轨道固定宽度 */
|
|
.joystick-track.vertical{width:60px;height:300px;}
|
|
.joystick-track.horizontal{width:100%;max-width:300px;height:60px;}
|
|
|
|
.joystick-handle{
|
|
position:absolute;
|
|
width:50px;
|
|
height:50px;
|
|
background-color:#4a90e2;
|
|
border-radius:50%;
|
|
cursor:grab;
|
|
transition:transform .1s ease-out,background-color .2s ease;
|
|
user-select:none;
|
|
-webkit-user-select:none;
|
|
border:2px solid rgba(255,255,255,.7);
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:center;
|
|
color:white;
|
|
font-weight:600;
|
|
font-size:14px;
|
|
}
|
|
|
|
.joystick-handle:active{
|
|
cursor:grabbing;
|
|
background-color:#357abd;
|
|
transform:scale(.95);
|
|
}
|
|
|
|
.joystick-wrapper.vertical .joystick-handle{left:5px;top:125px;}
|
|
.joystick-wrapper.horizontal .joystick-handle{left:calc(50% - 25px);top:5px;} /* 水平居中 */
|
|
|
|
.usage-info{
|
|
margin-top:30px;
|
|
color:#666;
|
|
font-size:14px;
|
|
text-align:center;
|
|
max-width:600px;
|
|
line-height:1.5;
|
|
}
|
|
|
|
/* 平板设备适配 */
|
|
@media (max-width:1024px){
|
|
.joystick-track.vertical{width:50px;height:250px;}
|
|
.joystick-track.horizontal{height:50px;}
|
|
.joystick-handle{width:40px;height:40px;font-size:12px;}
|
|
.joystick-wrapper.vertical .joystick-handle{left:5px;top:105px;}
|
|
.joystick-wrapper.horizontal .joystick-handle{left:calc(50% - 20px);top:5px;}
|
|
}
|
|
|
|
/* 手机设备适配 - 确保2个一行 */
|
|
@media (max-width:768px){
|
|
body{padding:10px 5px;}
|
|
.page-title{margin-bottom:15px;font-size:1.2rem;}
|
|
.joysticks-container{gap:10px;}
|
|
.joystick-item{width:calc(50% - 5px);} /* 调整间距计算 */
|
|
.joystick-track.vertical{width:45px;height:200px;}
|
|
.joystick-handle{width:35px;height:35px;font-size:12px;}
|
|
.joystick-wrapper.vertical .joystick-handle{left:5px;top:82.5px;}
|
|
.joystick-wrapper.horizontal .joystick-handle{left:calc(50% - 17.5px);top:5px;}
|
|
}
|
|
|
|
/* 小屏手机额外适配 */
|
|
@media (max-width:375px){
|
|
.joystick-track.vertical{width:40px;height:180px;}
|
|
.joystick-handle{width:30px;height:30px;font-size:11px;}
|
|
.joystick-wrapper.vertical .joystick-handle{left:5px;top:75px;}
|
|
.joystick-wrapper.horizontal .joystick-handle{left:calc(50% - 15px);top:5px;}
|
|
.joystick-label{font-size:12px;}
|
|
}
|
|
|
|
.button-container {
|
|
display: flex;
|
|
gap: 50px;
|
|
margin-bottom: 20px;
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
|
|
.control-btn {
|
|
padding: 12px 24px;
|
|
background: #fff;
|
|
color: #2196F3;
|
|
border: 2px solid #2196F3;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.control-btn:active {
|
|
background-color: #2196F3;
|
|
color: white;
|
|
}
|
|
|
|
.connection-status {
|
|
margin-bottom: 15px;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.status-connected {
|
|
background-color: #e8f5e9;
|
|
color: #2e7d32;
|
|
}
|
|
|
|
.status-disconnected {
|
|
background-color: #ffebee;
|
|
color: #c62828;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body onload="socket_init()">
|
|
<h1 class="page-title">控制器</h1>
|
|
|
|
<div class="connection-status status-disconnected" id="connectionStatus">未连接</div>
|
|
|
|
<div class="button-container">
|
|
<button class="control-btn" id="blink">blink</button>
|
|
<button class="control-btn" id="dir">jump</button>
|
|
</div>
|
|
|
|
<div class="joysticks-container">
|
|
<!-- 第一行第一个:水平摇杆 -->
|
|
<div class="joystick-item">
|
|
<div class="joystick-label">摇摆</div>
|
|
<div id="roll" class="joystick-wrapper horizontal"></div>
|
|
</div>
|
|
|
|
<!-- 第一行第二个:水平摇杆 -->
|
|
<div class="joystick-item">
|
|
<div class="joystick-label">左右</div>
|
|
<div id="joy_x" class="joystick-wrapper horizontal"></div>
|
|
</div>
|
|
|
|
<!-- 第二行第一个:垂直摇杆 -->
|
|
<div class="joystick-item">
|
|
<div class="joystick-label">高度</div>
|
|
<div id="height" class="joystick-wrapper vertical"></div>
|
|
</div>
|
|
|
|
<!-- 第二行第二个:垂直摇杆 -->
|
|
<div class="joystick-item">
|
|
<div class="joystick-label">前后</div>
|
|
<div id="joy_y" class="joystick-wrapper vertical"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var socket; // WebSocket实例
|
|
var isConnected = false; // 连接状态
|
|
|
|
// 更新连接状态显示
|
|
function updateConnectionStatus(connected) {
|
|
isConnected = connected;
|
|
const statusElement = document.getElementById('connectionStatus');
|
|
if (connected) {
|
|
statusElement.textContent = '已连接';
|
|
statusElement.className = 'connection-status status-connected';
|
|
} else {
|
|
statusElement.textContent = '未连接';
|
|
statusElement.className = 'connection-status status-disconnected';
|
|
}
|
|
}
|
|
|
|
// 初始化WebSocket连接
|
|
function socket_init() {
|
|
try {
|
|
// 根据环境选择合适的连接方式
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
socket = new WebSocket(wsProtocol + '//' + window.location.hostname + ':81/');
|
|
|
|
// 连接成功事件
|
|
socket.onopen = function() {
|
|
console.log('WebSocket连接已建立');
|
|
updateConnectionStatus(true);
|
|
};
|
|
|
|
// 接收消息事件
|
|
socket.onmessage = function(event) {
|
|
console.log('收到消息:', event.data);
|
|
// 可以在这里处理从服务器返回的消息
|
|
};
|
|
|
|
// 连接关闭事件
|
|
socket.onclose = function() {
|
|
console.log('WebSocket连接已关闭');
|
|
updateConnectionStatus(false);
|
|
// 尝试重连
|
|
setTimeout(socket_init, 3000);
|
|
};
|
|
|
|
// 错误事件
|
|
socket.onerror = function(error) {
|
|
console.error('WebSocket错误:', error);
|
|
updateConnectionStatus(false);
|
|
};
|
|
} catch (e) {
|
|
console.error('WebSocket初始化失败:', e);
|
|
updateConnectionStatus(false);
|
|
// 重试连接
|
|
setTimeout(socket_init, 3000);
|
|
}
|
|
}
|
|
|
|
// 摇杆控制器类
|
|
class Joystick {
|
|
constructor(containerId, direction = 'vertical', min = -100, max = 100) {
|
|
this.container = document.getElementById(containerId);
|
|
this.direction = direction;
|
|
this.id = containerId;
|
|
this.isDragging = false;
|
|
this.startPos = 0;
|
|
this.currentPos = 0;
|
|
this.centerPos = 0;
|
|
this.min = min;
|
|
this.max = max;
|
|
this.range = max - min;
|
|
|
|
this.createElements();
|
|
this.handle = this.container.querySelector('.joystick-handle');
|
|
this.track = this.container.querySelector('.joystick-track');
|
|
this.initDimensions();
|
|
this.bindEvents();
|
|
this.setHandlePosition(this.centerPos);
|
|
}
|
|
|
|
createElements() {
|
|
const track = document.createElement('div');
|
|
track.className = `joystick-track ${this.direction}`;
|
|
|
|
const handle = document.createElement('div');
|
|
handle.className = 'joystick-handle';
|
|
handle.textContent = '0';
|
|
|
|
track.appendChild(handle);
|
|
this.container.appendChild(track);
|
|
}
|
|
|
|
initDimensions() {
|
|
if (this.direction === 'vertical') {
|
|
this.trackSize = this.track.offsetHeight;
|
|
this.handleSize = this.handle.offsetHeight;
|
|
this.centerPos = (this.trackSize - this.handleSize) / 2;
|
|
} else {
|
|
this.trackSize = this.track.offsetWidth;
|
|
this.handleSize = this.handle.offsetWidth;
|
|
this.centerPos = (this.trackSize - this.handleSize) / 2;
|
|
}
|
|
}
|
|
|
|
clamp(value, min, max) {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
updateValue(position) {
|
|
const offset = this.direction === 'vertical'
|
|
? this.centerPos - position
|
|
: position - this.centerPos;
|
|
|
|
const rawPercentage = (offset / this.centerPos) * 100;
|
|
const rawValue = this.clamp(Math.round(rawPercentage), -100, 100);
|
|
|
|
const mappedValue = Math.round(
|
|
((rawValue + 100) / 200) * this.range + this.min
|
|
);
|
|
|
|
this.handle.textContent = mappedValue;
|
|
this.dispatchValueChange({raw: rawValue, mapped: mappedValue});
|
|
}
|
|
|
|
dispatchValueChange({raw, mapped}) {
|
|
const event = new CustomEvent('valuechange', {
|
|
detail: {
|
|
rawValue: raw,
|
|
mappedValue: mapped,
|
|
id: this.id,
|
|
joystick: this
|
|
},
|
|
bubbles: true
|
|
});
|
|
this.container.dispatchEvent(event);
|
|
}
|
|
|
|
setHandlePosition(pos) {
|
|
const minPos = 0;
|
|
const maxPos = this.trackSize - this.handleSize;
|
|
const constrainedPos = this.clamp(pos, minPos, maxPos);
|
|
|
|
if (this.direction === 'vertical') {
|
|
this.handle.style.top = `${constrainedPos}px`;
|
|
} else {
|
|
this.handle.style.left = `${constrainedPos}px`;
|
|
}
|
|
|
|
this.updateValue(constrainedPos);
|
|
}
|
|
|
|
returnToCenter() {
|
|
if (!this.isDragging) {
|
|
const currentPosition = parseFloat(
|
|
this.direction === 'vertical'
|
|
? this.handle.style.top
|
|
: this.handle.style.left
|
|
) || this.centerPos;
|
|
|
|
const distance = this.centerPos - currentPosition;
|
|
|
|
if (Math.abs(distance) < 1) {
|
|
this.setHandlePosition(this.centerPos);
|
|
return;
|
|
}
|
|
|
|
const step = distance * 0.5;
|
|
this.setHandlePosition(currentPosition + step);
|
|
requestAnimationFrame(() => this.returnToCenter());
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
this.handle.addEventListener('mousedown', (e) => {
|
|
this.isDragging = true;
|
|
this.startPos = this.direction === 'vertical' ? e.clientY : e.clientX;
|
|
this.currentPos = parseFloat(
|
|
this.direction === 'vertical'
|
|
? this.handle.style.top
|
|
: this.handle.style.left
|
|
) || this.centerPos;
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (this.isDragging) {
|
|
const delta = (this.direction === 'vertical' ? e.clientY : e.clientX) - this.startPos;
|
|
this.setHandlePosition(this.currentPos + delta);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (this.isDragging) {
|
|
this.isDragging = false;
|
|
this.returnToCenter();
|
|
}
|
|
});
|
|
|
|
this.handle.addEventListener('touchstart', (e) => {
|
|
this.isDragging = true;
|
|
this.startPos = this.direction === 'vertical'
|
|
? e.touches[0].clientY
|
|
: e.touches[0].clientX;
|
|
this.currentPos = parseFloat(
|
|
this.direction === 'vertical'
|
|
? this.handle.style.top
|
|
: this.handle.style.left
|
|
) || this.centerPos;
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener('touchmove', (e) => {
|
|
if (this.isDragging) {
|
|
const delta = (this.direction === 'vertical'
|
|
? e.touches[0].clientY
|
|
: e.touches[0].clientX) - this.startPos;
|
|
this.setHandlePosition(this.currentPos + delta);
|
|
e.preventDefault();
|
|
}
|
|
}, { passive: false });
|
|
|
|
document.addEventListener('touchend', () => {
|
|
if (this.isDragging) {
|
|
this.isDragging = false;
|
|
this.returnToCenter();
|
|
}
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
this.initDimensions();
|
|
if (!this.isDragging) {
|
|
this.setHandlePosition(this.centerPos);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 控制状态对象
|
|
const controlState = {
|
|
blink: 0,
|
|
dir: "stop",
|
|
roll: 0,
|
|
height: 30,
|
|
joy_y: 0,
|
|
joy_x: 0
|
|
};
|
|
|
|
// 统一发送函数
|
|
function sendAllData() {
|
|
if (!isConnected) {
|
|
console.log('未连接,无法发送数据');
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
dir: controlState.dir,
|
|
roll: controlState.roll,
|
|
height: controlState.height,
|
|
joy_y: controlState.joy_y,
|
|
joy_x: controlState.joy_x,
|
|
blink: controlState.blink,
|
|
stable: 1,
|
|
'mode': 'basic',
|
|
};
|
|
|
|
try {
|
|
socket.send(JSON.stringify(data));
|
|
console.log('发送数据:', data);
|
|
} catch (e) {
|
|
console.error('发送数据失败:', e);
|
|
updateConnectionStatus(false);
|
|
}
|
|
|
|
if (controlState.blink === 1) controlState.blink = 0;
|
|
if (controlState.dir === "jump") controlState.dir = "stop";
|
|
}
|
|
|
|
// 初始化摇杆实例
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const joysticks = [
|
|
new Joystick('roll', 'horizontal', -30, 30),
|
|
new Joystick('height', 'vertical', 0, 60),
|
|
new Joystick('joy_y', 'vertical'),
|
|
new Joystick('joy_x', 'horizontal')
|
|
];
|
|
|
|
document.querySelectorAll('.joystick-wrapper').forEach(wrapper => {
|
|
wrapper.addEventListener('valuechange', (e) => {
|
|
controlState[e.detail.id] = e.detail.mappedValue;
|
|
sendAllData();
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.control-btn').forEach(btn => {
|
|
btn.addEventListener('mousedown', (e) => {
|
|
btn.style.backgroundColor = '#2196F3';
|
|
btn.style.color = 'white';
|
|
|
|
switch(e.target.id) {
|
|
case 'blink': controlState.blink = 1; break;
|
|
case 'dir': controlState.dir = "jump"; break;
|
|
}
|
|
sendAllData();
|
|
});
|
|
|
|
btn.addEventListener('mouseup', () => {
|
|
btn.style.backgroundColor = '';
|
|
btn.style.color = '';
|
|
});
|
|
|
|
btn.addEventListener('touchstart', (e) => {
|
|
btn.style.backgroundColor = '#2196F3';
|
|
btn.style.color = 'white';
|
|
|
|
switch(e.target.id) {
|
|
case 'blink': controlState.blink = 1; break;
|
|
case 'dir': controlState.dir = "jump"; break;
|
|
}
|
|
sendAllData();
|
|
e.preventDefault();
|
|
});
|
|
|
|
btn.addEventListener('touchend', (e) => {
|
|
btn.style.backgroundColor = '';
|
|
btn.style.color = '';
|
|
e.preventDefault();
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |