摘要:手機屏幕朝上,水平靜止放置,軸重力加速度為,為。當手機水平放置,撥動手機,使其慢慢旋轉,重力加速度的數據并沒有變化。四元數的基本數學方程為其中表示旋轉角度,表示旋轉軸。四元數表示一個完整的旋轉。
前言
隨著智能硬件的普及,手機,平板,PC甚至路邊的電子廣告牌,現代瀏覽器已經無處不在。在瀏覽器里編織出我們自己的一片天地已經輕車熟路,但是這還不夠,H5賦予了瀏覽器太多的新特性,等待我們去使用。這篇文章介紹利用手機瀏覽器的羅盤API,在PC的瀏覽器實時地繪制一個3D盒模型。
這種炫酷的玩法叫做“多屏互動”,就像是把手機當做游戲手柄,PC顯示器當做電視機,不過這些都是在瀏覽器里實現的。
先上效果圖
(測試機是刷了小米系統的裂了屏幕的HTC霹靂2+Chrome瀏覽器)
源碼請戳這里:https://coding.net/u/OverTree...
本地測試過程:
在PC上,使用命令 node index.js,自動打開項目主頁。(請關閉ADsafe,如有虛擬機,請停用虛擬網卡)
創建一個“房間”并自動進入“房間”。
用手機掃描“房間”內任意位置的二維碼。
確保手機和PC可以相互PING通
客戶端(瀏覽器) 1. 手機瀏覽器端ADsafe是個很好用的去廣告軟件,但是會阻止本機IP訪問,可能造成項目首頁打不開,所以請先暫時關閉
本程序會自動獲取本機IP,如果有虛擬網卡,IP地址可能獲取不正確
一個物體在空間內的旋轉體位,都可以用一個方向向量(x,y,z)和旋轉角度(angle)來表示。也就是CSS3transform的rotate3d(x,y,z,angle)這個函數的4個參數。
想要在瀏覽器里方便的繪制一個立體模型的的旋轉,重點就是利用手機瀏覽器的H5新特性去獲取手機旋轉狀態的數據,然后轉化成這4個參數。
1.1 重力感應APIdevicemotion 顧名思義設備運動
其實不僅僅有重力感應的數據,還有移動加速度,擺動角度。
不過這個接口傾向于運動時瞬間的數據展示,靜止時,除了重力加速度,其他數據(移動加速度,擺動角度)基本為0。
window.addEventListener("devicemotion", deviceMotionHandler, true); function deviceMotionHandler(evt){ if(evt.accelerationIncludingGravity){ document.body.innerHTML = "x軸加速度: " + evt.accelerationIncludingGravity.x + "
" + "y軸加速度: " + evt.accelerationIncludingGravity.y + "
" + "z軸加速度: " + evt.accelerationIncludingGravity.z + "
" } if(evt.rotationRate ){ document.body.innerHTML += "x軸扭轉: " + evt.rotationRate.beta + "
" + "y軸扭轉: " + evt.rotationRate.gamma + "
" + "z軸扭轉: " + evt.rotationRate.alpha + "
" } }
(魅族老機型,安卓4.4.4的自帶瀏覽器對此API支持不完全,請另外安裝QQ瀏覽器)
在手機瀏覽器里運行以上代碼,并稍微晃動,會看到打印數據狂跳。
拿到了數據,接下來開始觀察規律。
手機屏幕朝上,水平靜止放置,Z軸重力加速度為9.8,Y,X為0。
手機屏幕朝下,水平靜止放置,Z軸重力加速度為-9.8,Y,X為0。
手機話筒朝下,豎直靜止放置,Y重力加速度為9.8, X,Z為0。
手機話筒朝上,豎直靜止放置,Y重力加速度為-9.8, X,Z為0。
手機右側朝上,豎直靜止放置,X重力加速度為9.8, Y,Z為0。
手機左側朝下,豎直靜止放置,X重力加速度為-9.8, Y,Z為0。
那么手機的空間坐標如下圖:
箭頭指向都是坐標正方向。
當手機開始傾斜,X,Y,Z軸的加速度分量都有值,且絕對值都小于9.8。根據分量的數值,是可以算出手機在三維空間的傾斜狀態,只不過這個計算過程復雜,而且在手機運動時,重力加速度的值并不準確表達當前傾斜。一般不用這個數據去計算手機在三維空間的傾斜。
當手機水平放置,撥動手機,使其慢慢旋轉,重力加速度的數據并沒有變化。
所以,重力感應的這個API,只能獲取設備當前的傾斜狀態,而無法獲取設備的旋轉方向。而一些簡單的功能,比如搖一搖,晃一晃,就可以用這個接口去實現。
利用重力感應的API,可以輕松利用高中數學的反三角函數,實現XY二維平面的旋轉,效果如下:
代碼如下:
function deviceMotionHandler(evt){ var angle = Math.atan2( 0 - evt.accelerationIncludingGravity.x , evt.accelerationIncludingGravity.y ).toFixed(2) / Math.PI * 180 ; }
這個 angle 就可以直接應用在DOM的CSS屬性transform:rotate(angle deg)上。
1.2 羅盤APIwindow.addEventListener("deviceorientation", deviceOrientationHandler, true); function deviceMotionHandler(evt){ document.body.innerHTML = "z軸旋轉(羅盤方向) alpha: " + event.alpha + "
" + "y軸旋轉 gamma: " + event.gamma + "
" + "x軸旋轉 beta: " + event.beta }
重點來了,deviceorientation能夠很好的表現物體在空間中的狀態,旋轉方向,傾斜角度,無論是靜止還是運動或者加速運動。
這里要和devicemotion 的 evt.rotationRate區分一下,雖然都有alpha,gamma,beta 但是 devicemotion 描述的是旋轉變化了的角度值,物體角度變化才會有數據,靜止了之后就變為0,而 deviceorientation 的是描述是靜止時的角度值。
這三個數值的單位都是deg,如何轉化為CSS3 transform:rotate3d(x,y,z,angle) 的4個參數,對于沒有任何3D知識的前端狗來說是個挺麻煩的問題。
現在要引入一個概念:四元數
四元數是個高階復數 q = [w,x,y,z]。
四元數的基本數學方程為 :
q = cos (a/2) + i(x sin(a/2)) + j(y sin(a/2)) + k(z * sin(a/2)) 其中a表示旋轉角度,(x,y,z)表示旋轉軸。
四元數表示一個完整的旋轉。
四元數可以由各軸旋轉角(alpha,beta,gamma)求得。
四元數可以轉換旋轉軸(x,y,z)和旋轉角度(angle)。
作為初試,本篇并不深入討論四元數的具體定義,難點是獲取四元數[w,x,y,z]。
好在官方提供了旋轉角(alpha,beta,gamma)轉換成四元數的方法
https://w3c.github.io/deviceo...
在這個頁面內搜索 getQuaternion
另外我根據數學公式反求,寫了一個四元數轉(x,y,z,angle) 的函數 getAcQuaternion
代碼如下:
var degtorad = Math.PI / 180; function getQuaternion( alpha, beta, gamma ) { //官方求四元數方法 var _x = beta ? beta * degtorad : 0; // beta value var _y = gamma ? gamma * degtorad : 0; // gamma value var _z = alpha ? alpha * degtorad : 0; // alpha value var cX = Math.cos( _x/2 ); var cY = Math.cos( _y/2 ); var cZ = Math.cos( _z/2 ); var sX = Math.sin( _x/2 ); var sY = Math.sin( _y/2 ); var sZ = Math.sin( _z/2 ); var w = cX * cY * cZ - sX * sY * sZ; var x = sX * cY * cZ - cX * sY * sZ; var y = cX * sY * cZ + sX * cY * sZ; var z = cX * cY * sZ + sX * sY * cZ; return [ w, x, y, z ]; } function getAcQuaternion( _w, _x, _y, _z ) { //我的四元數轉旋轉軸和旋轉角度方法 var rotate = 2 * Math.acos(_w)/degtorad ; var x = _x / Math.sin(degtorad * rotate/2) || 0; var y = _y / Math.sin(degtorad * rotate/2) || 0; var z = _z / Math.sin(degtorad * rotate/2) || 0; return {x:x,y:y,z:z,rotate:rotate}; } function deviceMotionHandler(evt){ // deviceorientation 事件處理函數 var qu = getQuaternion(evt.alpha,evt.beta,evt.gamma); var rotate3d = getAcQuaternion(qu[0],qu[1],qu[2],qu[3]); // rotate3d的參數已經有了,隨你處理咯。我是把他送給服務器,交給PC,在PC上顯示旋轉 }1.3 校準
這里有個3D里的概念,攝像機位置。我們的PC顯示器就是一個攝像機。只能被動的從某一個角度展示拍攝的景象。正常情況下,手機所在平面應該和顯示器所在平面平行,且垂直于地平面的角度。就好比是,攝像機正對著手機正面拍攝。
如果校準的時候手機并沒有垂直于地平面,攝像機的位置就不一定是正前方了。這時候展示的畫面并不是水平同步的了。
如下圖所示,校準時,手機屏幕朝上。這時候攝像機位置就在天花板上了,你看到的成像就是俯視圖。
同理,校準時,手機屏幕朝下,這時候攝像機的位置就是在地上,往上拍攝,你看到的成像就是仰視圖。
總結起來就是:校準時,手機屏幕朝著哪里,攝像機就在那里拍攝著屏幕,一動不動。
1.4 兼容性demo的兼容性測試并不理想
在iOS平臺上測試良好,且流暢。
在安卓平臺上,除了chrome瀏覽器之外的瀏覽器,會出現各種問題,主要表現在羅盤數據不準確。
而chrome瀏覽器并沒有掃一掃功能,因為在國外并不流行這個玩意。所以在安卓平臺上就很蛋疼,還要多裝一個我查查,才能完整體驗。
(如果出現旋轉不準確的問題,可以嘗試校準羅盤,大概就是拿著手機畫8。百度一下方法有很多)
代碼如果有兼容寫法,或者有其他兼容問題請賜教,可以在coding上私信我(OverTree ),不勝感激。
2. PC瀏覽器端PC瀏覽器的作用就是能夠顯示房間信息,創建房間。
顯示房間,創建時間,參與人數,點擊進入。
創建一個房間,成功后自動進入房間。
在房間內,接受服務器轉發的手機端的消息,并作出相應動作,包括上線,校準,旋轉,下線。
上線時,安排就坐(隱藏二維碼,顯示模型)
校準時,重新設置模型的顯示角度。
旋轉時,就旋轉咯。
下線時,重新顯示二維碼(顯示二維碼,隱藏模型)
重點是房間里的事情。所以這里就只介紹進入房間發生的事吧。
首先房間參數要正確,至少有房間編號。
房間路由:
/room/[roomNumber]
roomNumber是一串16位隨機字符串。
座位路由:
/room/[roomNumber]/[seatNumber]
var uri = win.location.pathname.split("/"),roomNumber; function initUrlData(){ if(uri.length>=3 && uri[1] == "room"){ roomNumber = uri[2]; document.title = "虛擬房間 "+ roomNumber + "號" return 1; }else{ window.location.href = "/index"; return 0; } } function initWebSocket(){ var wsUri = "ws://"+ window.location.hostname +":<%= config.wsport %>"+"/ws/room"; //這里用了一個ejs的占位符,已便在服務器更改websocket端口時可以及時使用正確端口。 var websocket = new WebSocket(wsUri); websocket.onopen = function(evt) { websocket.send(JSON.stringify({room:roomNumber})); }; //鏈接建立后,發送一個消息,表明在哪個房間 websocket.onclose = function(evt) { }; websocket.onmessage = function(evt) { parseMessage(evt.data) //解析數據 }; websocket.onerror = function(evt) { }; //綁定了這些處理函數之后,websocket開始建立鏈接,而不是 New 的時候開始建立 } $(".room-place .qrcode").each(function(index,item){ $(item).qrcode({ "size": 200, "color": "#3a3", "text": window.location.origin + "/room/" + roomNumber + "/" + (index+1) }); //這里用jQuery的插件,jquery-qrcode 按照座位路由初始化二維碼 })2.2 純CSS3立體模型
做為一名普通的前端人員,想要畫一個3D的模型,按照最熟悉的方法就是用CSS3了。
(如果是用Three.js的大神請跳過本節)
不過要很快畫出一個六面體出來,還是需要想一想的,畢竟這個技能很少用。
畫一個長方體
對這樣的css有什么要吐槽的么?
這樣的stylesheet簡直是刀耕火種時期的
如果用sass寫法,那么只需要寫一次#box和多層嵌套就可以了。
效果如下:
如果我們使用webGL去繪制的話,導入一些現成的3D模型,無論物體還是人物,都可以360度無死角的玩弄于手掌了。
(如果有蒼老師的模型,想想還有點小激動呢,VR的感覺說來就來啊 - -)
接下來就是等待來自手機端的旋轉信息,x,y,z,angle,使#box進行transform旋轉就是了。
$seat.find("#box"). css("transform","rotate3d(" + (-parseFloat(content.x))+"," //取反 + (+parseFloat(content.y))+"," + (-parseFloat(content.z))+"," //取反 + content.rotate +"deg)");
不取反的話,旋轉是錯誤的。我曾多次嘗試給不同的坐標取反,最終得出這個取反方法,是唯一顯示正常的組合。
無法理解這兩個取反,猜測是因為css的x,y,z的坐標和物理設備x,y,z的坐標方向有差異吧。畢竟顯示器是平面的,他的x,y,z的定義不能和手機傳感器一致。
2.3 校準PC端的校準就簡單多了,在#box外套一層div.adjust。
當接受來自手機端的校準信息 x,y,z,angle,設置外套的 div.adjust 的旋轉為 x,y,z,-angle 就好了。
$seat.find(".adjust"). css("transform","rotate3d(" + (-parseFloat(content.x))+"," + (+parseFloat(content.y))+"," + (-parseFloat(content.z))+"," + (-parseFloat(content.rotate)) +"deg)"); //取反
當然,這個adjust的樣式至少包含以下樣式
.adjust{ position: absolute; transform-style:preserve-3d; }2.4 兼容性
PC端的兼容性就好多了,只要是現代H5瀏覽器基本上沒有兼容性問題。
服務端 1.數據結構這個服務只做臨時數據的保存和消息轉發。
臨時數據:比如,各端的webSocket連接句柄,房間信息等,我把它們放在global全局對象下,就好比是共享內存,訪問方便,速度快。
global.ShareMem = { rooms:{ "12345678":{ //房間號做為key,方便查找 player:[{socket:connection,place:place}], //手機端數組:連接句柄,座位號 projector:[], //PC端數組 id:"12345678", startTime:Date.now(), maxplayer:2, //最多座位數 type:"ffffd" //房間類型 } } };2.webServer
如果您是nodejs的大神,或者在用koajs、express等nodejs框架,請跳過本大節。因為我用原生的nodejs寫了一遍webServer,雖然重復造輪子不好,但是復習復習webServer的基本知識,還是不錯的,本節適合新手入門。
包含知識點:header解析,靜態文件查找,gzip,文件hash計算,狀態碼。
/API /funMap.js /*http功能函數集合*/ /xxx.js /socketAPI /funMap.js /*webSocket功能函數集合*/ /xxx.js /Util /*工具目錄,獲取本地IP,打開默認瀏覽器*/ /webRoot /common /*公共資源目錄*/ /js /lib /css /m /*移動端html,js,css等*/ /p /*PC端html,js,css等*/ /index.js /*入口文件*/ /config.js /*配置文件,端口號,ws最大數據包大小等*/ /socketServer.js /*webSocket處理函數*/ /webServer.js2.2 webServer
基本規則是這樣的,搭建靜態服務器,靜態資源正常讀取返回,html文件用ejs渲染后返回。
由于ejs的原因,html文件并沒有被修改,但是渲染后的內容被修改,比如,更改了ws的端口,但是html文件沒有修改。所以不能使用Last-Modified來判斷是文件是否最新,而是要根據返回內容有沒有被改變來判斷,所以要用Etag。
Etag需要根據內容算出hash值,一般用md5計算。
返回內容之前,需要進行gzip壓縮,用來節省帶寬。90KB的jquery.min.js可以被gzip到30KB,壓縮才是王道。
因為手機端和PC端執行的是完全不同的代碼,所以要判斷從客戶端傳過來的user-agent是否包含Mobile字符串,以來區分客戶端是PC還是手機,以便返回正確的資源。
通過簡單的約定,來區分靜態文件和REST請求
if (libPath.extname(pathName) == "") { //如果路徑沒有擴展名 if(params.length<=2){ pathName += "/"; //訪問根目錄 }else if(params[1]=="api"){ //訪問以api開頭 parseAPI(params,req,res); //功能函數 return ; }else{ pathName = params[1]+".html"; } }
我在這里做了一個簡單的框架,在API目錄或者socketAPI目錄下新增js文件,一個js文件對應一個處理函數,然后在funMap.js中聚合為一個Map,方便查找函數,也容易隔離和修改函數名。
var funMap = { "room":require("./room"), "changeName":require("./xxx"), "changeName2":require("./xxxyyy") }; module.exports = funMap;
客戶端訪問時就可以通過 /api/[functionName] 來訪問想要的服務了。
3 webSocketServernodejs本身并沒有提供webSockerServer的模塊,所以需要另外安裝一個。
在npm install的時候會安裝一個ws模塊,require("ws") 就可以用了。用法與http模塊相似,都用 createServer({options},MainHandlerFunction) 創建服務,只是ws多了幾個參數。
主要是port,注意不要和webserver端口重復。
還有一個 maxPayload 就是單個ws數據包最大大小,單位是bytes,自己估計項目傳輸數據時候數據包大小。默認值是65535 即 64KB。一般webSocket用于小包傳輸,不用太大,我設置了1024 , 1KB。
主處理函數MainHandlerFunction,在有客戶端連接進來時會傳入一個參數connection,這個對象內容非常豐富,不看手冊,可以打印出來也慢慢研究。
成功建立連接的方法就是要connection綁定message方法。
由于wsSocket訪問是可以帶著url的,所以我們可以用url隔離不同的功能函數,而不是去解析message主體。
var connectHandler = function(connection){ // :4002/api/Function1 var URIarray = connection.upgradeReq.url.split("/") if(funMap[URIarray[2]]){ funMap[URIarray[2]](connection); }else{ connection.send("{err:Function Not Found!!}"); } }3.1 消息,廣播,保活
每當有ws連接進來,都有類似文件描述符的id來區分每個不同的連接。
connection._ultron.id 用它可以區分自己與別人的連接,很有用。
//消息格式 function msgPack(){ return JSON.stringify({ "who":arguments[0], // Mobile , PC "place":arguments[1], // 座位 "dowhat":arguments[2], // "connect","ready","message","lost" "content":arguments[3]||"" // 內容 }) } //以room為單位廣播,廣播房間內所有角色 function boradCast(room,msg,ignore){ room.projector.forEach(function(item,index){ if(ignore&&ignore._ultron.id===item.socket._ultron.id){ // console.log("ignore!!!") // 忽略自己不發送給自己 } else{ try{ item.socket.send(msg); }catch(e){ console.log(e); } } }); room.player.forEach(function(item,index){ if(ignore&&ignore._ultron.id===item.socket._ultron.id){ // console.log("ignore!!!") // 忽略自己不發送給自己 } else{ try{ item.socket.send(msg); }catch(e){ console.log(e); } } }); }
為了檢查客戶端是否掉線,在建立連接時手動加入保活機制,方法很簡單:
給客戶端發送空消息時lastkeeplife為1,只要客戶端返回任意消息,那么更新lastkeeplife為0,如果5秒之內,沒有任何回復判定為掉線。
如果客戶端掉線,那么關閉連接,從連接池中移除。并廣播掉線消息給房間內其他角色。
var keeplifeHandler = setInterval(function(){ if(lastkeeplife == 0){ connection.close(); connection.emit("close"); clearInterval(keeplifeHandler); } try{ lastkeeplife = 0; connection.send("{}"); }catch(e){ console.log("keep live error! "+ e +" "); connection.close(); connection.emit("close"); clearInterval(keeplifeHandler); } },5000) connection.on("close",function(msg){ if(keeplifeHandler){ //關閉保活循環 clearInterval(keeplifeHandler); } console.log("close!",roomid,place); var room = global.ShareMem.rooms[roomid]; if(!room) return; //從連接池移除連接句柄 if(platform === PC){ room.projector.forEach(function(item,index){ if(item.socket === connection){ room.projector.splice(index,1); return false; } }) }else{ room.player.forEach(function(item,index){ if(item.socket === connection){ room.player.splice(index,1); return false; } }) } //發送掉線消息 boradCast( room, msgPack(platform,place,"lost") , connection ); });
iOS設備如果鎖屏,會發送斷開信息給服務器,而安卓不會。想要斷開鏈接,必須等到默認120秒超時后關閉。
ws初始化時并沒有提供初始化timeout的配置。通過修改
ws._server.timeout = 1000;//1秒超時
并不會生效。問題來了,怎么修改才能設置超時時間呢?
目前只能用上述比較捉急的方法來及時斷開掉線設備。
最后多屏互動已經不是新鮮的東西了,我做這個Demo還是受chrome實驗室一個叫做【光劍出鞘】的項目的啟發。因為體驗時需要手機端和PC同時翻-墻,導致體驗差,然后自己才想做一個。做出來的時候感覺好酷炫,好神奇,好興奮。
后續還是有很多可以拓展和改進的,希望最終可以變為一個成熟的產品,而不是僅僅止步于Demo。
無需Flash實現圖片裁剪——HTML5中級進階
5個提高Node.js應用性能的技巧
瀏覽器存儲及使用
作者信息
作者來自力譜宿云 LeapCloud 團隊_UX成員:王詩詩 【原創】
首發地址:https://blog.maxleap.cn/archi...
王詩詩,前端新人,專職前端工作兩年。曾供職于AMI做底層軟件開發。喜歡分析H5代碼,追崇用簡單的CSS,構建精美動效,做前端之前,這些是業余愛好。現任職于MaxLeap UX 組,負責MaxWon 的開發和維護。現熱衷于Real-time WebApp。
歡迎關注微信訂閱號:MaxLeap_yidongyanfa
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87810.html
摘要:比如就會報出警告,并執行出錯。視頻的寬高,并不會因為填寫的數值比例不合法而失真。通過綁定事件,來獲取視頻片段數據,并在內存中累積。執行之后會停止觸發事件。錄制結束后,把累計的片段數據保存為對象,并從瀏覽器下載存為視頻文件。 前言 HTML5的權限越來越大了,瀏覽器可以直接調用攝像頭、麥克風了,好激動啊。我們要用純潔的HTML代碼造出自己的天地。 視頻采集 本篇介紹的栗子 都是在chro...
摘要:最近公司設計了一款基于自己產品的投屏類應用,能夠使得用戶通過我們的產品平臺來設計啟用用于投屏功能的頁面,實現留言互動等功能。使用場景投屏類應用操作簡便,用戶僅需要填寫簡單信息便可實現上墻服務,進行活動現場創意互動,點燃現場情緒。 最近公司設計了一款基于自己產品的投屏類H5應用,能夠使得用戶通過我們的產品平臺來設計啟用用于投屏功能的H5頁面,實現留言互動等功能。整體的實現和用戶體驗都是基...
閱讀 3931·2021-11-24 10:46
閱讀 1820·2021-11-16 11:44
閱讀 2296·2021-09-22 16:02
閱讀 1405·2019-08-30 15:55
閱讀 1134·2019-08-30 12:46
閱讀 570·2019-08-28 18:31
閱讀 2765·2019-08-26 18:38
閱讀 1098·2019-08-23 16:51