纯H5+JS实现网页摇杆 esp32摄像头网页控制

最近基于ESP32 Arduino 的摄像头例程设计一个网页控制摄像头舵机方向,在网页绘制了一个摇杆并将摇杆坐标发送给esp32。参考了许多网友的资料,终于实现了基础功能,在此记录并分享一下源代码,注释已经尽量详细了。

包括了PC端鼠标坐标的输出,移动端触摸坐标的输出,禁用右键与长按菜单,摄像头视频显示,uri数据发送到esp32等部分。

效果图:

源代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>AIQI-Motor-Demo</title>
		<style type="text/css">
			* {
				-webkit-touch-callout: none;
				-webkit-user-select: none;
				-khtml-user-select: none;
				-moz-user-select: none;
				-ms-user-select: none;
				user-select: none;
			}
			body{
				background-color: #282c35;
			}
			button{
				border-radius: 0.9375rem;
				border: none;
				background-color: #21242d;
				box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
				border: 2px solid #aaffff;
				color: #aaffff;
				font-size:1.625rem;
				width: 8rem;
			}
			button:active {
			  background-color: #282c35;
			  box-shadow: 0 4px 16px 0 rgba(0,0,0,0.2), 0 3px 20px 0 rgba(0,0,0,0.19);
			  transform: translateY(4px);
			}
			#EYE_Button{
				border: 2px solid #ff5500;
				color: #ff5500;
			}
			#buttons{
				height:auto;
				width:auto;
				text-align:center;
				margin: 2.5rem;
			}
			#stream-container,#rocker-container{
				height:auto; 
				width:auto;
				text-align:center;
				margin: 0rem;
			}
			#rocker-container{
				margin: 3rem;
			}
			#coordiv{
				border: 2px solid #aaffff;
				border-radius: 0.9375rem;
			}
			
		</style>
	</head>
	<body>
		<div id="buttons">
			<button id="Rotate_Button" onclick="RotateButton()">Rotate</button>
			<button id="EYE_Button" onclick="EYEButton()">Eye-ON</button>
		</div>
		<div id="stream-container">
			<img id="stream" src="" style="width: 40rem;height: 30rem;" >
		</div>
		<div id="rocker-container">
			<canvas id="coordiv" width="600" height="600">您的浏览器不支持canvas</canvas>
			<div id="point-loc" style="color: #aaffff;"></div>
		</div>
	</body>
	<script>
			const view = document.getElementById('stream')		//摄像头显示图
			const buttons = document.getElementById('buttons')		//按钮容器
			const rockercontainer = document.getElementById('rocker-container')	//摇杆容器
			const Rotate_Button = document.getElementById('Rotate_Button')	//显示图旋转按钮
			const EYE_Button = document.getElementById('EYE_Button')	//摄像头开关按钮
			var coordiv = document.getElementById('coordiv');	//摇杆画布
			
			var baseHost = document.location.origin  //获取URL域名(ip)与端口号
			var streamUrl = baseHost + ':81'	//esp32 摄像头arduino例程 摄像头流数据端口
			
			//摄像头显示图 角度旋转 每次90°
			var ImgRotate = 0;
			function RotateButton(){
				ImgRotate += 90;
				if(ImgRotate >= 360) ImgRotate = 0;
				view.style.transform = "rotate("+ImgRotate+"deg)";
				if(ImgRotate == 90 || ImgRotate == 270)view.style.margin = "6rem";
				else view.style.margin = "0rem"
			}
			
			//摄像头开关 通过img src指向控制
			function EYEButton(){
				const streamEnabled = EYE_Button.innerHTML === 'Eye-ON'
				if (streamEnabled) {
					view.src = `${streamUrl}/stream`
					EYE_Button.style.border = "2px solid #4CAF50";
					EYE_Button.style.color = "#4CAF50";
					EYE_Button.innerHTML = 'Eye-OFF';
				}else{
					// view.src = ` `; //清空图片或保持当前图片
					window.stop();	//停止页面刷新
					EYE_Button.style.border = "2px solid #ff5500";
					EYE_Button.style.color = "#ff5500";
					EYE_Button.innerHTML = 'Eye-ON';	
				}
			}
			
			//画布尺寸(需要与body内设置相同) 和 摇杆中心坐标(画布尺寸/2)
			var coordiv_w = 600;
			var coordiv_h = 600;
			var center_x = coordiv_w/2;
			var center_y = coordiv_h/2;
			
			
			//摇杆初始化 显示中心坐标和手柄
			var loc = "当前位置 x:"+parseInt(center_x/(center_x/5))+",y:"+parseInt(center_y/(center_y/5));
			document.getElementById("point-loc").innerHTML = loc;
			var ctx=coordiv.getContext("2d");	//画布初始化
			new_coordinate(center_x,center_y);	//中心位置绘制圆形手柄 并显示坐标

			//禁止手机双指缩放		原代码直接复制粘贴
			var lastTouchEnd = 0;
			document.documentElement.addEventListener('touchend', function (event) {
			  var now = Date.now();
			  if (now - lastTouchEnd <= 300) {
				event.preventDefault();
			  }
			  lastTouchEnd = now;
			}, false);
			//禁止手机双击缩放		原代码直接复制粘贴
			document.documentElement.addEventListener('touchstart', function (event) {
			  if (event.touches.length > 1) {
			    event.preventDefault();
			  }
			}, false);
			
			
			//移动端浏览器 手指点击相关事件
			coordiv.ontouchstart = function(e){//手指头触摸屏幕上的事件
				var touch = e.touches[0] //获取第一个触点
			    var x = this.offsetLeft		//e为当前事件 this为当前元素
			    var y = this.offsetTop		//获取当前元素距离页面边界的距离
				var NowX = parseInt((touch.pageX - x))	//获取全局点击坐标 计算画布内点击坐标
				var NowY = parseInt((touch.pageY - y))
				new_coordinate(NowX,NowY)	//重绘圆形手柄 并显示当前坐标
			    document.ontouchmove = function(e){//手指头在屏幕上滑动触发的事件
					touch = e.touches[0]; //获取第一个触点
					NowX = parseInt((touch.pageX - x))  //计算画布内点击坐标
					NowY = parseInt((touch.pageY - y))
					new_coordinate(NowX,NowY) //重绘圆形手柄 并显示当前坐标
			    }
			    document.ontouchend = function(){//当手指从屏幕上离开的时候触发
			        //清除事件
					new_coordinate(center_x,center_y) //重绘圆形手柄 并显示当前坐标
			        document.ontouchstart = null //清空点击事件
			        document.ontouchmove = null
			    }	
			}
			
			//PC端浏览器 鼠标点击相关事件
			coordiv.onmousedown = function(e){//鼠标按下触发事件
			    var x = this.offsetLeft
			    var y = this.offsetTop
				var NowX = (e.clientX - x)
				var NowY = (e.clientY - y)
				new_coordinate(NowX,NowY)
			    document.onmousemove = function(e){//鼠标按下时持续触发事件
					NowX = (e.clientX - x)
					NowY = (e.clientY - y)
					new_coordinate(NowX,NowY)
			    }
			    document.onmouseup = function(){//鼠标抬起触发事件
			        //清除事件
					new_coordinate(center_x,center_y)
			        document.onmouseup = null
			        document.onmousemove = null
			    }	
			}
			var LastX = parseInt(center_x/(center_x/5))
			var LastY = parseInt(center_y/(center_y/5))
			//绘制摇杆圆形手柄 并显示当前坐标
			function new_coordinate(NowX,NowY){
				if(NowX>=coordiv_w)NowX=coordiv_w
				if(NowY>=coordiv_h)NowY=coordiv_h
				if(NowX<=0)NowX=0
				if(NowY<=0)NowY=0
				ctx.clearRect(0, 0, coordiv_w, coordiv_h)	//清空画布
				ctx.save()	//保存转态
				ctx.beginPath() //开启路径
				ctx.shadowBlur=10 //阴影宽度
				ctx.shadowColor="#00ffff"	//阴影颜色
				ctx.strokeStyle = "#aaffff";  //设置边线的颜色
				ctx.arc(NowX, NowY, center_x/4,0,2*Math.PI) //画圆
				ctx.fillStyle="#21242d" //填充颜色
				ctx.fill()	//填充路径
				ctx.stroke()	//绘制路径
				ctx.closePath() // 关闭路径
				ctx.restore()	//为画布重置为最近保存的图像状态
				NowX = parseInt(NowX/(center_x/5))
				NowY = parseInt(NowY/(center_y/5))
				if(NowX!=LastX || NowY!=LastY)   //减小发送频率
				{
					//显示坐标
					var loc = "当前位置 x:"+NowX+",y:"+NowY
					document.getElementById("point-loc").innerHTML = loc
					//发送坐标
					const query = baseHost+"/rocker?var=rocker&val="+PrefixZero(NowX,2)+PrefixZero(NowY,2)
					fetch(query)
					  .then(response => {
					    console.log(`request to ${query} finished, status: ${response.status}`)
					  })
					LastX = NowX;
					LastY = NowY;
				}
			}
			
			/**
			* 功能:将数字按规定位数输出,不足的位补零。例如 PrefixZero(3,2) = 03
			* @param num: 被操作数
			* @param n: 固定的总位数
			*/
			function PrefixZero(num, n) {
			    return (Array(n).join(0) + num).slice(-n);
			}
			
			//禁用右键菜单
			window.addEventListener('contextmenu', function (e) {
				e.preventDefault()
			})
	</script>
</html>

本人为物联网实习生,前端代码只是了解,所以有错误部分或处理不好的部分,望路过的大佬批评指导。🙇‍

 

------------2020/6/5 修改-------修改坐标发送为每500ms发送一次---------

点击及触摸部分修改

var mouseTime;
//移动端浏览器 手指点击相关事件
coordiv.ontouchstart = function(e){//手指头触摸屏幕上的事件
	var touch = e.touches[0] //获取第一个触点
	var x = this.offsetLeft		//e为当前事件 this为当前元素
	var y = this.offsetTop		//获取当前元素距离页面边界的距离
	var NowX = parseInt((touch.pageX - x))	//获取全局点击坐标 计算画布内点击坐标
	var NowY = parseInt((touch.pageY - y))
	new_coordinate(NowX,NowY)	//重绘圆形手柄 并显示当前坐标
	document.ontouchmove = function(e){//手指头在屏幕上滑动触发的事件
		touch = e.touches[0]; //获取第一个触点
		NowX = parseInt((touch.pageX - x))  //计算画布内点击坐标
		NowY = parseInt((touch.pageY - y))
		new_coordinate(NowX,NowY) //重绘圆形手柄 并显示当前坐标
	}
	mouseTime = setInterval(function (){  //setInterval可一直执行内部函数
		RX_NowX_Y(NowX,NowY)
	}, 500);
	document.ontouchend = function(){//当手指从屏幕上离开的时候触发
	//清除事件
	clearInterval(mouseTime);   //清除setInterval的时间
	new_coordinate(center_x,center_y) //重绘圆形手柄 并显示当前坐标
	RX_NowX_Y(center_x,center_y)
	document.ontouchstart = null //清空点击事件
	document.ontouchmove = null
	}
}
			
//PC端浏览器 鼠标点击相关事件
coordiv.onmousedown = function(e){//鼠标按下触发事件
	var x = this.offsetLeft
	var y = this.offsetTop
	var NowX = (e.clientX - x)
	var NowY = (e.clientY - y)
	new_coordinate(NowX,NowY) //重绘圆形手柄 并显示当前坐标
	document.onmousemove = function(e){//鼠标按下时持续触发事件
		NowX = (e.clientX - x)
		NowY = (e.clientY - y)
		new_coordinate(NowX,NowY) //重绘圆形手柄 并显示当前坐标
	}
	mouseTime = setInterval(function (){  //setInterval可一直执行内部函数
		RX_NowX_Y(NowX,NowY)
	}, 500);
	document.onmouseup = function(){//鼠标抬起触发事件
		//清除事件
		clearInterval(mouseTime);   //清除setInterval的时间
		new_coordinate(center_x,center_y)
		RX_NowX_Y(center_x,center_y)
		document.onmouseup = null
		document.onmousemove = null
	}
}

摇杆绘制及发送修改

//绘制摇杆圆形手柄 并显示当前坐标
function new_coordinate(NowX,NowY){
	if(NowX>=coordiv_w)NowX=coordiv_w
	if(NowY>=coordiv_h)NowY=coordiv_h
	if(NowX<=0)NowX=0
	if(NowY<=0)NowY=0
	ctx.clearRect(0, 0, coordiv_w, coordiv_h)	//清空画布
	ctx.save()	//保存转态
	ctx.beginPath() //开启路径
	ctx.shadowBlur=10 //阴影宽度
	ctx.shadowColor="#00ffff"	//阴影颜色
	ctx.strokeStyle = "#aaffff";  //设置边线的颜色
	ctx.arc(NowX, NowY, center_x/4,0,2*Math.PI) //画圆
	ctx.fillStyle="#21242d" //填充颜色
	ctx.fill()	//填充路径
	ctx.stroke()	//绘制路径
	ctx.closePath() // 关闭路径
	ctx.restore()	//为画布重置为最近保存的图像状态
	NowX = parseInt(NowX/(center_x/5))
	NowY = parseInt(NowY/(center_y/5))
	//显示坐标
	var loc = "当前位置 x:"+NowX+",y:"+NowY
	document.getElementById("point-loc").innerHTML = loc
}
function RX_NowX_Y(NowX,NowY){//发送坐标
	if(NowX>=coordiv_w)NowX=coordiv_w
	if(NowY>=coordiv_h)NowY=coordiv_h
	if(NowX<=0)NowX=0
	if(NowY<=0)NowY=0
	NowX = parseInt(NowX/(center_x/5))
	NowY = parseInt(NowY/(center_y/5))
	const query = baseHost+"/rocker?            
    var=rocker&val="+PrefixZero(NowX,2)+PrefixZero(NowY,2)
    fetch(query)
	.then(response => {
		 console.log(`request to ${query} finished, status: ${response.status}`)
	})
}

 

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
好的,以下是一个基本的搜索框下拉列表的实现步骤: 1. HTML结构:在HTML中创建一个输入框和一个下拉列表的容器,下拉列表的容器初始设置为隐藏状态。 ```html <div class="search-container"> <input type="text" placeholder="请输入搜索关键字"> <div class="search-list-container"> <ul class="search-list"></ul> </div> </div> ``` 2. CSS样式:设置输入框的样式和下拉列表容器的样式,下拉列表容器的初始状态设置为 `display:none` 。 ```css .search-container { position: relative; } input[type="text"] { width: 300px; height: 30px; padding: 5px; border: 1px solid #ccc; border-radius: 5px; } .search-list-container { position: absolute; top: 35px; left: 0; z-index: 99; width: 300px; max-height: 200px; overflow-y: auto; background: #fff; border: 1px solid #ccc; border-radius: 5px; display: none; } ``` 3. JS交互:监听输入框的键盘输入事件,在输入框中输入内容时,向后台发送请求获取匹配的搜索结果,将结果渲染到下拉列表中,并将下拉列表容器设置为显示状态。 ```javascript const input = document.querySelector('input[type="text"]'); const searchListContainer = document.querySelector('.search-list-container'); const searchList = document.querySelector('.search-list'); input.addEventListener('input', function(e) { const keyword = e.target.value; if (keyword.trim()) { // 发送请求获取匹配的搜索结果 const searchResults = getSearchResults(keyword); renderSearchList(searchResults); searchListContainer.style.display = 'block'; } else { searchList.innerHTML = ''; searchListContainer.style.display = 'none'; } }); function getSearchResults(keyword) { // 向后台发送请求获取搜索结果 const results = ['搜索结果1', '搜索结果2', '搜索结果3']; return results; } function renderSearchList(results) { let html = ''; results.forEach(result => { html += `<li>${result}</li>`; }); searchList.innerHTML = html; } ``` 以上就是一个简单的搜索框下拉列表的实现方法,您可以根据自己的需求进行样式和交互的修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值