웹으로 근본적으로 전환하면서 브라우저에서 바로 멋진 작업을 수행 할 수 있습니다. 이 자습서에서는 Generic Sensor API를 사용하여 스마트 폰을 실시간 추적 기능이 있는 포인터로 전환합니다.
우리가 만들 것입니다 :
https://thumbs.gfycat.com/UnhealthyWateryAmericanwarmblood-mobile.mp4
https://medium.com/better-programming/track-your-smartphone-in-2d-with-javascript-1ba44603c0df
전제 조건
현재로서는 Generic Sensor API가 iOS에서 아직 지원되지 않습니다. 또한 일부 Android 스마트 폰에는 필요한 센서가 없습니다.
그러나 Chrome DevTools에 있는 시뮬레이션 센서를 사용하여 이 자습서를 계속 진행할 수 있습니다.
USB를 통해 Android 스마트 폰에서 Chrome의 콘솔 출력을 볼 수도 있지만 추가 설정이 필요합니다.
일반 센서 API에는 보안 컨텍스트가 필요합니다. 따라서 HTTPS가 필요합니다. 센서 시뮬레이터로 로컬 호스트에서 작업하거나 스마트 폰으로 온라인 코드 편집기 (예 : Repl.it)로 작업 할 수 있습니다.
참고 : 튜토리얼에 대한 모든 내용은 이 Repl에서 찾을 수 있습니다. 여기에서 코드를 탐색 및 편집하고 데모를 시도 할 수 있습니다.
스마트 폰 추적
일반 controller.html 파일과 해당 controller.js 스크립트로 시작해 보겠습니다.
<html> | |
<head></head> | |
<body> | |
<script src="controller.js"></script> | |
</body> | |
</html> |
일반 센서 API는 여러 센서를 지원합니다. 그러나 요구 사항에 대해서는 AbsoluteOrientationSensor를 사용합니다.
MDN 웹 문서에 따르면 AbsoluteOrientationSensor는 "지구의 기준 좌표계와 관련하여 장치의 물리적 방향을 설명하는"센서 융합 API입니다.
여러 실제 센서의 데이터를 결합하여 데이터를 결합하고 필터링 하여 사용하기 쉽도록 새로운 가상 센서를 구현할 수 있습니다.이를 퓨전 센서라고 합니다. 이 경우 온보드 자력계, 가속도계 및 자이로 스코프의 데이터가 AbsoluteOrientationSensor의 구현에 사용됩니다.
아래는 이 가상 센서와 인터페이스 하기 위한 코드입니다. 그리고 그게 다야!
const sensor = new AbsoluteOrientationSensor({frequency: 60}); | |
sensor.addEventListener("reading", (e) => handleSensor(e)); | |
sensor.start(); | |
function handleSensor(e){ | |
console.log(e.target.quaternion); | |
} |
먼저, 센서 객체가 설정된 주파수 (센서를 읽는 속도 및 해당 핸들 센서 콜백이 발생하는 속도)로 초기화됩니다. 그런 다음 읽기 프로세스를 시작합니다.
페이지를 새로 고친 후 휴대 전화를 움직여 다음 출력을 확인하십시오. 쿼터니언 스트림이 나타납니다.
그러나 쿼터니언이란 무엇입니까?
"쿼터니언은 복소수를 확장하는 숫자 시스템입니다." — 위키 백과
오일러 각 대신 공간에서 물체의 방향을 설명하는 대체 방법으로 사용할 수 있습니다. 쿼터니언은 이를 이용한 계산이 계산 비용이 적게 들기 때문에 게임 개발에 광범위하게 사용됩니다.
그러나 간단하게 하기 위해 이를 보다 직관적인 오일러 각도로 변환하겠습니다. 다음은 변환 공식에 따라 JavaScript 구현입니다. 2 차원으로 추적 할 때 피치는 생략되었습니다.
function toEuler(q) { | |
let sinr_cosp = 2 * (q[3] * q[0] + q[1] * q[2]); | |
let cosr_cosp = 1 - 2 * (q[0] * q[0] + q[1] * q[1]); | |
let roll = Math.atan2(sinr_cosp, cosr_cosp); | |
let siny_cosp = 2 * (q[3] * q[2] + q[0] * q[1]); | |
let cosy_cosp = 1 - 2 * (q[1] * q[1] + q[2] * q[2]); | |
let yaw = Math.atan2(siny_cosp, cosy_cosp); | |
return [yaw, roll]; | |
} |
handleSensor 함수를 업데이트하여 위의 함수를 사용하여 변환 된 오일러 각도를 인쇄하십시오.
function handleSensor(e){ | |
let quaternion = e.target.quaternion; | |
let angles = toEuler(quaternion); | |
console.log(angles); | |
} |
라디안 단위의 각도 출력이 표시되어야 합니다. 전화를 돌리면 직관적으로 변경됩니다. 다음은 포인터를 2D로 추적하는 데 사용할 치수입니다.
스마트 폰의 방향을 실시간으로 추적하는 데 필요한 센서 데이터를 얻기 위해 일반 센서 API와 성공적으로 인터페이스 했습니다. 이제 이 변화하는 각도를 화면에 투사 된 움직임으로 변환해야 합니다.
그러나 먼저 모든 거리를 측정 할 초기 시작 위치를 설정하는 보정 방법이 필요합니다.
let initPos; | |
let calibrate = true; | |
document.body.addEventListener("click", () => {calibrate = true}) | |
function handleSensor(e){ | |
let quaternion = e.target.quaternion; | |
let angles = toEuler(quaternion); | |
if(calibrate){ | |
initPos = angles; | |
calibrate = false; | |
} | |
} |
컨트롤러 페이지 본문을 클릭하면 전화기의 현재 방향이 모든 각도 및 거리를 측정하는 시작점으로 설정됩니다.
이동 한 상대 거리를 계산하려면 시작점에서 각도의 변화를 사용하여 간단한 삼각법이 필요합니다.
차이를 취할 때 올바른 값을 보장하기 위해 랩핑하는 방법이 필요합니다 (예 : 180 ° 및 -180 ° 포인트).
코드는 다음과 같습니다.
function calcDist(angle, initAngle) { | |
angle = (angle - initAngle) * (180 / Math.PI); | |
angle = angle < 0 ? angle + 360 : angle; | |
angle = angle > 180 ? angle - 360 : angle; | |
let dist = Math.round(-800 * Math.tan(angle * (Math.PI / 180))); | |
return dist; | |
} |
참고 : 최종 계산에서 숫자 800은 캔버스에서 컨트롤러의 가상 거리를 결정합니다. 현실에서는 이것이 의미가 없으며 대신 운동 감도를 변경하는 데 사용할 수 있습니다.
위의 함수를 사용하여 계산 된 거리를 인쇄하도록 handleSensor 함수를 업데이트하십시오.
function handleSensor(e){ | |
let quaternion = e.target.quaternion; | |
let angles = toEuler(quaternion); | |
if(calibrate){ | |
initPos = angles; | |
calibrate = false; | |
} | |
let dist = angles.map((angle, i) => calcDist(angle, initPos[i])); | |
console.log(dist); | |
} |
그리고 그게 다야!
이제 스마트 폰의 움직임을 실시간으로 추적하여 화면 움직임을 위한 거리 측정으로 변환 할 수 있습니다.
스마트 폰을 가리키는
간단한 Node.js 서버, 일부 SocketIO 매직 및 HTML 캔버스 요소를 사용하면 위의 거리 측정으로 스마트 폰을 여러 컨트롤러를 지원하는 디지털 포인터로 바꿀 수 있습니다.
참고 :이 기사에서는 센서 API 활용에 중점을 두기 때문에 SocketIO 설명에 대해 자세히 설명했지만 코드는 설명이 필요 없습니다. 자세한 내용은 설명서를 참조하십시오.
https://thumbs.gfycat.com/UnhealthyWateryAmericanwarmblood-mobile.mp4
서버
이 간단한 서버는 공용 디렉토리에서 HTML 페이지를 제공하고 SocketIO를 통해 연결된 모든 웹 클라이언트에 컨트롤러 데이터를 보내 포인터를 캔버스에 렌더링 합니다.
const express = require('express'); | |
var app = express(); | |
var server = require("http").Server(app); | |
var io = require("socket.io")(server); | |
var path = require('path'); | |
app.use(express.static(path.join(__dirname, "public"))); | |
app.get('/', (req, res) => { | |
res.sendFile(path.join(__dirname + "/public/canvas.html")); | |
}); | |
app.get('/controller', (req, res) => { | |
res.sendFile(path.join(__dirname + '/public/controller.html')); | |
}); | |
server.listen(3000); | |
let controllers = {}; | |
setInterval(()=>io.emit("update", Object.values(controllers)), 30); | |
io.on("connection", (client) => { | |
client.on("controller", () => { | |
client.on("update", (dist) => { | |
controllers[client.id] = dist; | |
}); | |
client.on("disconnect", () => { | |
delete controllers[client.id]; | |
}); | |
}); | |
}); |
연결된 컨트롤러 클라이언트 목록을 저장하면 여러 컨트롤러에 대한 지원을 추가 할 수 있습니다.
결과적으로, 컨트롤러의 배열과 이동 된 거리는 setInterval을 사용하여 일정한 속도로 연결된 모든 클라이언트에 전송됩니다.
컨트롤러
컨트롤러는 센서 데이터를 읽고, 거리를 계산하고, 서버로 전송하여 브로드 캐스트 할 서버 (보통 소켓 IO 서버와의 추가 통신 포함)를 전송합니다.
<html> | |
<head> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.dev.js"></script> | |
<link rel="stylesheet" href="styles.css"> | |
</head> | |
<body> | |
<p>Press to calibrate.</p> | |
<script> | |
const sensor = new AbsoluteOrientationSensor({frequency: 60}); | |
sensor.addEventListener("reading", (e) => handleSensor(e)); | |
sensor.start(); | |
let initPos; | |
let calibrate = true; | |
document.body.addEventListener("click", () => {calibrate = true}) | |
var socket = io(); | |
socket.on('connect', function(){ | |
socket.emit("controller"); | |
}); | |
function handleSensor(e){ | |
let q = e.target.quaternion; | |
let angles = toEuler(q); | |
if(calibrate){ | |
initPos = angles; | |
calibrate = false; | |
} | |
let dist = angles.map((angle, i) => calcDist(angle, initPos[i])); | |
socket.emit("update", dist); | |
} | |
function toEuler(q) { | |
let sinr_cosp = 2 * (q[3] * q[0] + q[1] * q[2]); | |
let cosr_cosp = 1 - 2 * (q[0] * q[0] + q[1] * q[1]); | |
let roll = Math.atan2(sinr_cosp, cosr_cosp); | |
let siny_cosp = 2 * (q[3] * q[2] + q[0] * q[1]); | |
let cosy_cosp = 1 - 2 * (q[1] * q[1] + q[2] * q[2]); | |
let yaw = Math.atan2(siny_cosp, cosy_cosp); | |
return [yaw, roll]; | |
} | |
function calcDist(angle, initAngle) { | |
angle = (angle - initAngle) * (180 / Math.PI); | |
angle = angle < 0 ? angle + 360 : angle; | |
angle = angle > 180 ? angle - 360 : angle; | |
let dist = Math.round(-800 * Math.tan(angle * (Math.PI / 180))); | |
return dist; | |
} | |
</script> | |
</body> | |
</html> |
디지털 캔버스
캔버스 페이지는 서버에서 받은 계산 된 거리를 캔버스에서 원형 포인터로 렌더링 합니다.
<html> | |
<head> | |
<link rel="stylesheet" href="styles.css"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.dev.js"></script> | |
</head> | |
<body> | |
<canvas class="canvas"></canvas> | |
<script> | |
let controllers = []; | |
let colours = ["#f44336", "#9C27B0", "#3F51B5", "#00BCD4", "#4CAF50", "#CDDC39","#FFC107", "#FF5722"]; | |
let canvas = document.querySelector(".canvas"); | |
let ctx = canvas.getContext("2d"); | |
canvas.setAttribute("width", window.innerWidth); | |
canvas.setAttribute("height", window.innerHeight); | |
window.addEventListener("resize", () => { | |
canvas.setAttribute("width", window.innerWidth); | |
canvas.setAttribute("height", window.innerHeight); | |
}); | |
var socket = io(); | |
socket.on('connect', function(){ | |
socket.on('update', function(data){ | |
controllers = data; | |
}); | |
}); | |
function draw(data){ | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
for(let i = 0; i < controllers.length; i++){ | |
let distances = controllers[i]; | |
let x = distances[0] + window.innerWidth/2; | |
let y = distances[1] + window.innerHeight/2; | |
ctx.beginPath(); | |
ctx.arc(x, y, 20, 0, 2 * Math.PI); | |
ctx.fillStyle = colours[i]; | |
ctx.fill(); | |
ctx.closePath(); | |
} | |
requestAnimationFrame(() => draw()); | |
} | |
draw(); | |
</script> | |
</body> | |
</html> |
대부분 설명이 필요하지만 그리기 함수를 살펴 보겠습니다.
function draw(data){ | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
for(let i = 0; i < controllers.length; i++){ | |
let distances = controllers[i]; | |
let x = distances[0] + window.innerWidth/2; | |
let y = distances[1] + window.innerHeight/2; | |
ctx.beginPath(); | |
ctx.arc(x, y, 20, 0, 2 * Math.PI); | |
ctx.fillStyle = colours[i]; | |
ctx.fill(); | |
ctx.closePath(); | |
} | |
requestAnimationFrame(() => draw()); | |
} |
여러 컨트롤러의 경우 컨트롤러 배열이 반복되고 해당 거리는 캔버스 좌표계가 시작되는 왼쪽 위 모서리가 아닌 중앙에서 포인터를 시작할 수 있도록 하는 양만큼 오프셋 됩니다.
그런 다음 사용 가능한 다음 색상으로 Canvas API를 사용하여 원으로 렌더링 됩니다. 자세한 내용은 문서를 참조하십시오.
마지막으로 requestAnimationFrame 함수가 호출되어 다음 다시 그리기 전에 브라우저가 이전 작업을 다시 수행하도록 지시합니다. 이주기는 포인터가 스마트 폰에서 실시간으로 계속 움직입니다.
결론
스마트 폰 포인터가 작동하면 축하합니다! 그러나 다음은 무엇입니까?
의사 소통 도구일까요? 레이저 포인터보다 똑똑한 대안? 또는 스마트 폰이 페인트 브러시가 되는 협업 디지털 캔버스 인 Paintr와 같은 것이 있습니다.
이것은 주머니에서 우리 모두가 가지고 다니는 것과 상호 작용하는 새로운 매체입니다. 이런 종류의 설정으로 가능성은 무한합니다.
등록된 댓글이 없습니다.