水準器アプリの試作2
スマートフォンをテーブルに置くとテーブルの傾きが分かる。
そんな「水準器アプリ」を試作しました。
本記事は、「水準器アプリの試作1」の続き記事です。
今回試作する「水準器」は、大きく2つの機能で構成されています。
先ず、加速度センサーから、スマ-トフォンの傾き(方向、角度)を計算する、計算機能。
次に、計算結果をビジュアル表示する、表示機能です。
- 計算機能:加速度センサー計算
- 表示機能:スマホ傾き表示
前回「水準器アプリの試作1」で、「表示機能」について説明しました。
今回「水準器アプリの試作2」で、「計算機能」について説明します。
座標系
Fig.1 は、スマートフォンの座標系です。
Fig.1 左側が、スマートフォンの正面図です。
Fig.1 右側は、スマートフォンの側面図です。
座標系の各軸には色が付いています。
- x 座標:赤
- y 座標:緑
- z 座標:青
加速度センサー
Fig.2 は、スマートフォンを前傾させた図です。
- スマートフォンを「静かに」前傾させています
そのため、重力(G)以外の加速度が発生していない状態です。
Fig.2 の状況で、加速度センサーからの出力値 A は、以下となります(注1)。
(注1)実際には、上記値を中心に、値が細かく振動しています。
傾きの検出
スマートフォンを「静かに」傾けた場合、加速度センサーからの出力値 A は、重力のみを検出します。
重力(G)は、常に鉛直方向を向いているので、重力を検出することで、スマートフォンの傾き(方向、角度)が分かります。
- 重力(G)を検出 → スマートフォンの傾きが計算できる
水準器アプリの計算機能
水準器アプリでは、加速度センサーで検出した重力(G) から、スマートフォンの傾き(方向、角度)を計算します。
傾き方向
スマートフォンの傾き方向の各成分は、以下となります。
傾き角度
スマートフォンの傾き角度は、次式で計算します。
式 (4) の詳細は、以下の記事をご参照ください。
閾値(Threshold)
傾き角度が小さい場合、傾き角度を「0°」とします。
Fig.2 を見ると、傾き角度が小さくなると、G sin θ の値が小さくなることが分かります。
実際にθ を、1° で計算すると、以下となります(注3)。
(注3)重力加速度(G)の値を、9.8 m/s2 としました
そこで、閾値(Threshold)を以下としました。
- Threshold: 0.15
次の g を計算し、それが Threshold 以下であれば、「0°」とみなしています。
プログラム
プログラム1
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Level</title> <style type="text/css"> * { margin: 0; padding: 0; border: 0; } </style> <script> var canvas; var ctx; // context var i0, j0; // var C = []; // The center of the canvas var S = []; // Position of the square var edgLen = 300; // Edge length[pixel] var oldTheta = 0; // θ var oldG_x = 0; // var oldG_y = 0; // var THRESHOLD = 0.15; // var osCorrect = 1; // -1:Android, 1:Others window.addEventListener("devicemotion", function(evt){ // var accel = evt.accelerationIncludingGravity; var G_x = osCorrect*accel.x; var G_y = osCorrect*accel.y; var G_z = osCorrect*accel.z; // var G = Math.sqrt(G_x*G_x+G_y*G_y+G_z*G_z); var theta = 180/Math.PI*Math.acos(-G_z/G); // theta = 0.9*oldTheta + 0.1*theta; G_x = 0.9*oldG_x + 0.1*G_x; G_y = 0.9*oldG_y + 0.1*G_y; oldTheta = theta; oldG_x = G_x; oldG_y = G_y; // drawSquare(G_x, G_y); writeTheta(theta); }, true); function writeTheta(theta) { ctx.font = "bold 50pt sans-serif"; ctx.fillStyle = "blue"; ctx.fillText(theta.toFixed(1).toString(10),C[0]-50,C[1]+20); } function drawSquare(G_x, G_y) { var g = Math.sqrt(G_x*G_x+G_y*G_y); if (g>THRESHOLD) { var r = edgLen/2; var P = []; P[0] = S[0] + r + r*G_x/g; P[1] = S[1] + r - r*G_y/g; drawGradation(P); } else { ctx.fillStyle = "green"; ctx.fillRect(S[0], S[1], edgLen, edgLen); ctx.fill(); } } function drawGradation(P) { var Q = []; Q[0] = 2*C[0] - P[0]; // S[0] + (C[0]-S[0]) - (P[0]-C[0]) Q[1] = 2*C[1] - P[1]; // S[1] + (C[1]-S[1]) + (C[1]-P[1]) // var grad = ctx.createLinearGradient(P[0], P[1], Q[0], Q[1]); grad.addColorStop(0, "black"); grad.addColorStop(1, "white"); ctx.fillStyle = grad; // ctx.fillRect(S[0], S[1], edgLen, edgLen); ctx.fill(); } window.onload = function() { canvas = document.getElementById("canvas"); // canvas.setAttribute("width", window.innerWidth); canvas.setAttribute("height", window.innerHeight); ctx = canvas.getContext("2d"); if (!ctx) return; // i0 = window.innerWidth/2; j0 = window.innerHeight/2; C[0] = i0; C[1] = j0; S[0] = C[0] - edgLen/2; S[1] = C[1] - edgLen/2; // if (navigator.userAgent.match(/Android/)) osCorrect = -1; } </script> </head> <body> <canvas id="canvas" style="background-color:green;"></canvas> </body> </html>
プログラム1 の解説
グローバル変数
var canvas; var ctx; // context var i0, j0; // var C = []; // The center of the canvas var S = []; // Position of the square var edgLen = 300; // Edge length[pixel] var oldTheta = 0; // θ var oldG_x = 0; // var oldG_y = 0; // var THRESHOLD = 0.15; // var osCorrect = 1; // -1:Android, 1:Others
プログラム1 の13 行目、2 次元変数 C は、Canvas の中心座標を保存する変数です。
スマートフォンの傾き(方向、角度)は、スマートフォンの中心にある正方形領域に描画します。
14 行目、2 次元変数 S は、その正方形領域のCanvas 上での位置を保存する変数です。
詳細は、記事「水準器アプリの試作1」をご参照ください。
15 行目、変数 edgLen は、正方形領域の一辺の長さです。
16 – 18 行、各値を平滑化するための変数です。
- oldTheta: 傾き角度(θ)
- oldGx :傾き方向(Gx)
- oldGy :傾き方向(Gy)
詳細は、記事「スマホセンサーのノイズ軽減」をご参照ください。
19 行目、変数 THRESHOLD は、式 (6) の閾値です。
20 行目、変数 osCorrect は、スマートフォンがAndroid の場合、補正するための変数です。
詳細は、記事「加速度センサーのAndroid 補正」をご参照ください。
window.onload 関数
window.onload = function() { canvas = document.getElementById("canvas"); // canvas.setAttribute("width", window.innerWidth); canvas.setAttribute("height", window.innerHeight); ctx = canvas.getContext("2d"); if (!ctx) return; // i0 = window.innerWidth/2; j0 = window.innerHeight/2; C[0] = i0; C[1] = j0; S[0] = C[0] - edgLen/2; S[1] = C[1] - edgLen/2; // if (navigator.userAgent.match(/Android/)) osCorrect = -1; }
プログラム1 の86 – 91 行、詳細は、記事「水準器アプリの試作1」をご参照ください。
93 行目、スマートフォンがAndroid の場合、osCorrect に -1 を代入します。
詳細は、記事「加速度センサーのAndroid 補正」をご参照ください。
devicemotion イベント
window.addEventListener("devicemotion", function(evt){ // var accel = evt.accelerationIncludingGravity; var G_x = osCorrect*accel.x; var G_y = osCorrect*accel.y; var G_z = osCorrect*accel.z; // var G = Math.sqrt(G_x*G_x+G_y*G_y+G_z*G_z); var theta = 180/Math.PI*Math.acos(-G_z/G); // theta = 0.9*oldTheta + 0.1*theta; G_x = 0.9*oldG_x + 0.1*G_x; G_y = 0.9*oldG_y + 0.1*G_y; oldTheta = theta; oldG_x = G_x; oldG_y = G_y; // drawSquare(G_x, G_y); writeTheta(theta); }, true);
プログラム1 の24 – 27 行、加速度センサーの値を得ています。
また、スマートフォンがAndroid の場合、補正処理しています。
29 – 30 行、傾き角度(θ)を計算しています。
32 – 37 行、各値(θ, Gx, Gy)の平滑化処理しています。
39 行目、drawSquare 関数を呼んでいます。
40 行目、writeTheta 関数を呼んでいます。
writeTheta 関数
function writeTheta(theta) { ctx.font = "bold 50pt sans-serif"; ctx.fillStyle = "blue"; ctx.fillText(theta.toFixed(1).toString(10),C[0]-50,C[1]+20); }
正方形の中心に、傾き角(θ)の値を表示しています。
drawSquare 関数
function drawSquare(G_x, G_y) { var g = Math.sqrt(G_x*G_x+G_y*G_y); if (g>THRESHOLD) { var r = edgLen/2; var P = []; P[0] = S[0] + r + r*G_x/g; P[1] = S[1] + r - r*G_y/g; drawGradation(P); } else { ctx.fillStyle = "green"; ctx.fillRect(S[0], S[1], edgLen, edgLen); ctx.fill(); } }
プログラム1 の50 行目、閾値処理のための変数 g を計算しています。
52 – 56 行、g が閾値より大なので、drawGradation 関数へ渡す引数を計算し、drawGradation 関数を呼んでいます。
58 – 60 行、g が閾値以下なので、正方形を「green」で塗ります。
drawGradation 関数
function drawGradation(P) { var Q = []; Q[0] = 2*C[0] - P[0]; // S[0] + (C[0]-S[0]) - (P[0]-C[0]) Q[1] = 2*C[1] - P[1]; // S[1] + (C[1]-S[1]) + (C[1]-P[1]) // var grad = ctx.createLinearGradient(P[0], P[1], Q[0], Q[1]); grad.addColorStop(0, "black"); grad.addColorStop(1, "white"); ctx.fillStyle = grad; // ctx.fillRect(S[0], S[1], edgLen, edgLen); ctx.fill(); }
drawGradation 関数は、記事「水準器アプリの試作1」をご参照ください。