最近基于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}`)
})
}