2014-11-12 ,

マルチプルタブハンドラ(Firefox拡張)

Feedlyでタブを一気に開くと、それを閉じるのが結構面倒くさい。タブをマウスのShift+クリックで範囲選択して、一気に閉じられたらなぁと思って調べてみた。

すでに入っていたTab Mix Plusにそんな機能は無いかなとShift+クリックしてみたらタブがピン留めされた。Ctrl+クリックはマージがどうのという機能らしいのだけどよく分からず。いずれにせよ、Tab Mix Plusにはそんな機能は無いようだ。

Google検索で調べた結果、Selecting multiple tabs, e.g. for mass window migrations によると、Multiple Tab HandlerかSession Managerを使うか、Bug 566510 – Allow multiselect operations on tabsの実現を待てということらしい。

マルチプルタブハンドラ (Multiple Tab Handler) :: Add-ons for Firefox を入れてみた。

すでにTab Mix Plusが入っているところにインストールすると、Tab Mix Plusで使われている操作と衝突するけどどうするかと聞かれた。素晴らしい気遣い! 当然マルチプルタブハンドラの操作だけを使うようにした。すると、タブを複数選択できるようになり、一括で閉じることが出来るようになった。

2014-11-12 , ,

Feedly Open All Unread Button Nov.2014

(2021-07-22追記:新しいのを作りました)

Feedlyに対する色々なユーザースクリプトが大分前に動かなくなったのを面倒くさいので放置していたのですが、重い腰を上げて未読を一気に別タブで開くのだけ実装しました。探せばどこかにあるのかもしれませんが、userscripts.orgが止まってからというものそれも面倒になりましたし、度々使えなくなるものなので、この際自分で調べて実装することにしました。

Feedlyサイト内の一つ一つのエントリーは.u0Entry(Title Only表示時)とか.u4Entry(Magazine表示時)とか.u5Entry(Cards表示時)とか.u100Frame(Full Articles表示時)とか、表示モードによって違うクラス名のdiv(以下、エントリーdiv)に囲まれるようですね。 エントリーdivではdata-で始まる属性で記事における様々な情報を表現しているようです。URLなんかはありますが、残念ながら既読未読の情報は無いようです。

未読だけを列挙するには、セレクターa.title.unreadにマッチするものを列挙すれば良さそうです。このa要素はエントリーdivの子孫ですが、何段下になるかは状況によって変わるようです。つまり、a.parentNode.parentNodeが必ずしもエントリーdivになるとは限りません。ただ、parentNodeをたどっていくといつかはエントリーdivにたどり着くようではあります。

記事のオリジナルURLは、aのhref=から求めても良いですし、エントリーdivのdata-alternate-link=から求めても良いでしょう。

URLを開いた後、記事を既読としてマークして一覧から消したいところです。Title Only表示の時はimg[title="Mark as read and hide"]にマッチする要素を、それ以外はspan.action[title="mark as read and remove from list"]にマッチする要素をクリックすることで実現できます。

後はUIです。開くボタンや一度に開く記事数のUIをページ内に追加したいです。しかしfeedlyが余分な要素を削除してしまう場合があるようだったので、1秒ごとに document.bodyの直下 (2014-11-13変更: .pageActionBarの直下) に挿入し直すようにしてみます。

以上を勘案して、次のようなスクリプトになりました。

function openUnreadEntries(limit){
    var unreads = document.querySelectorAll(".title.unread");
    var count = Math.min(unreads.length, limit || 5);
    for(var i = 0; i < count; ++i){
        // determine elements
        var title = unreads[i];
        var entry = (function(e){
            while(e && !e.getAttribute("data-alternate-link"))
                e = e.parentNode;
            return e;})(title.parentNode);

        // open url
        var url = (entry && entry.getAttribute("data-alternate-link")) ||
                title.getAttribute("href");
        window.open(url, "_blank");

        // mark as read and hide
        var readAndHideButton = entry && (
            entry.querySelector('.condensedTools img[title="Mark as read and hide"]') || //Title Only
            entry.querySelector('.action[title="mark as read and remove from list"]') //Magazine, Card, Full Articles
        );
        (readAndHideButton || entry || title).click();
    };
}
var BUTTON_CLASS_NAME = "openUnreadButton";
function createButton(){
    var div = document.createElement("div");
    div.style.display = "inline-block";
    div.style.border = "1px solid #bbb";
    div.style.borderRadius = "3px";
    div.style.verticalAlign = "top";
    div.className = "pageAction " + BUTTON_CLASS_NAME;

    div.innerHTML =
        '<input type="number" value="5" style="width:3em">'+
        '<input type="button" value="Open Unread">';

    var count = div.querySelector('input[type="number"]');
    var button = div.querySelector('input[type="button"]');
    button.addEventListener("click", function(e){
        openUnreadEntries(parseInt(count.value, 10));
    }, false);
    return div;
}
function setupUI(){
    var bars = document.getElementsByClassName("pageActionBar");
    for(var i = 0; i < bars.length; ++i){
        var bar = bars[i];
        if(!bar.querySelector("."+BUTTON_CLASS_NAME)){
            bar.insertBefore(createButton(), bar.firstChild);
        }
    }
}
setInterval(setupUI, 1000);

あとはこれをGreasemonkeyに登録して、 http://feedly.com/* で有効になるようにすればOKです。

2014-11-04 ,

JavaScriptペグソリティア

この間旅行に行ったときのこと。小さなレストラン兼お土産屋で食事をしたのですが、テーブルの上に木製のパズルが。挑戦してみたのですが全く解けませんでした。悔しかったので作りました。

JavaScript Peg Solitaire

ペグソリティアです。ペグ(黒いの)をドラッグして隣接する他のペグを飛び越えると、その飛び越えられたペグを取り除くことが出来ます。最終的にペグが一本になったら成功です。

English StyleでもEuropean Styleでも難しすぎて解ける気がしません。誰か解き方教えて!

編集機能が付いているので、簡単な問題を作って自分で解くのも楽しいです。自分の力量にあった問題を遊べますからね。出来た問題はURLで共有したり、ブログ等に埋め込むことも出来ます。

ほぼ一本道の簡単な問題(埋め込み)。


この辺りも簡単(URL共有)。

http://misohena.github.io/js_pegsolitaire/index.html?p=H+3+3+OPOPP__PP

穴15個くらいなら適当にやっても偶然解けたりしますね。


例によってソースはGitHubです。

2014-11-02 ,

JavaScript反転パズルゲーム

なんかこういうパズルってあったよなぁと思い出して作りました。

ずっと名前が思い出せなくて、少し作った後に検索してライツアウトだと思い出しました。そうそう、昔一時流行りましたよね。まぁ、私はあまりやってなかったんですけど。

その他のサンプルは使用例ページへ。ソースコードはGitHubへ。

問題が解けたらご褒美画像がもらえるような仕組みにしてみました。 でもスクリプトを見たら画像のURLが分かってしまうようだと興ざめなので、対策を施したバージョンも作ってあります。 一つは画像のファイル名に正解の手をエンコードした文字列を付加する方法。ランダムで出題できなくなりますが、一応解かないとファイル名が分かりません。正解の手は複数ある場合があるので、全ての正解をあらかじめ列挙しておく必要があります。意外と面倒くさい。 もう一つはサーバが出題・解答判定を行う方法。本当はセッション管理をして個別に問題を出題したかったのですが、面倒なのでIPアドレスと時刻などを使用して問題を決定します。解答が送られてきたときは、その問題が解答の手順で解けるかどうかを確認し、解ければ画像ファイルを送ります。 ググればSolverも見つかるようなパズルなので別にそこまでしなくても良いのでしょうが、別なゲームにも応用できそうなので少し考えてみました。

解法については未だ詳しくないのですが、5x5だけは確実に解ける方法を覚えました。

2014-10-30 ,

加速度センサーを使ったJavaScriptスキーゲーム

左右の傾きを検出できるようになったところなので何か応用を。……スキーゲームでも作りましょうか。

適当にどんな感じにするか決めて……(汚い字でスミマセン)

wpid-wp-1414652864075-150x150.jpeg

作ったのが以下(単独のプレイページはこちら)。


デバイス左右の傾きでスキーヤーが左右に動きます。ゲートをくぐって得点を競いましょう。

一応キーボードにも対応しました。あえて傾き操作のみにしようと思っていたのですが、開発中のテストプレイが面倒だったので対応しました。でも傾けるインタフェースに合わせてわざと操作しづらくしてあります。

ソースコードはGitHubで。

あ、そういえば、Nexus7(2012)で試したらdeviceorientationを使った方法だと姿勢が安定しなかったので、devicemotionを使って傾きを検出しています。Nexus5やXperia Z2だとどちらを使ってもそれほど違いは無かったのですが、Nexus7(2012)だと大きく違いが出ました。キャリブレーションすれば直るかもしれませんが、一応大丈夫な方を取りました。

スキーヤーの当たり判定は体の前半分にしかありません。なので、後ろ半分はポールに当たっても大丈夫です。

フィールドの座標系は上がY正方向、右がX正方向、原点はスタート地点下部中央という取り方をしてみました。慣れない座標系を使うと色々ミスをしますね。パパッと作りたい場合は普通にY正方向が画面下になる座標系の方が良かったかなぁと思ったり。

メモ書きではleftWallとrightWall(左右の黒い部分)というオブジェクトを見いだしていたのですが、実際に作っている途中でroad(中央の白い部分)にしました。壁だと左右を別々に管理しなければならず、かといって壁を延ばすときは同じ長さだけ伸ばすように注意しなければならないなど、少し無駄に面倒な気がしたので。

左右移動の速度調整が一番面倒でした。

あとはゲートの出現頻度。1分半(90000ms)までは100フレーム毎から40フレーム毎まで単調に減少していき、それ以降は(20+40*(100000/(100000+(time-90000))))フレーム毎という緩やかに漸近線をとりながら減少していくのを基本としつつランダムで±20フレームの揺れを入れてみました。2~3分くらいからもう少し沢山出た方が忙しくなって良いかもしれないけどどうかなぁ。

2014-10-28

JavaScriptでデバイスを左右に傾けて移動するユーザーインタフェース

デバイスの向きが取得できるようになったところなので、実際の応用を考えてみることにします。画面の中に何かが表示されていて、右に傾けたら右へ、左に傾けたら左へ移動するようなユーザーインタフェースというのはどうでしょう。

左右の傾きを検出するにはどうしたら良いでしょうか。

さて、そもそも「左右の傾き」とはいったい何でしょうか。

deviceorientationのgammaでしょうか? gammaはデバイスのY軸、つまり画面縦方向を軸とした回転角でした。確かに机の上にデバイスを水平において、少し左右に傾けたときの姿勢はY軸回りの回転だけで表されそうです。

しかしデバイスを手で持って顔の前で使っている場合はどうでしょう。左右に傾けるというのはZ軸(画面奥から手前に向けての軸)回りの回転ではないでしょうか。

仰向けに寝そべって天井に向けてデバイスを持っている場合はどうでしょう。これはY軸の回転で左右操作をしそうですね。人によってはZ軸で操作するかもしれません。

これらに共通するものは何でしょうか。人は左右の傾きを何を基準に入力しているのでしょうか。

おそらく、X軸(デバイス横方向の軸)の傾きではないでしょうか。つまり、X軸の正方向(右方向)が地を向けば右、逆に天を向けば左なのではないでしょうか。これはデバイス(板)の上に乗っているボールが重力に沿って左右に動く様を想像しているのかもしれません。

となれば、デバイスのX軸が地面に対して何度傾いているかを計算すれば、左右の傾きになりそうです。

デバイスの向きがdeviceorientationイベントのalpha, beta, gammaで表されるとき、デバイス座標rから地球座標r'への変換は次の式になるのでした。

r' = matRotZ(alpha)*matRotX(beta)*matRotY(gamma)*r

(rとr'は列ベクトル、matRotZ(alpha)はZ軸回りにalpha度回転する変換行列)

今知りたいのはデバイスのX軸正方向を地球座標へ変換したときにZ軸(地面に対して垂直な軸)方向が正になるか負になるかです。

デバイスのX軸正方向を代表する座標 r=[1,0,0] を変換元にして、行列を全て結合すると次のようになります。

r'= [ cos(alpha)*cos(gamma)-sin(alpha)*sin(beta)*sin(gamma) ]
    [ sin(alpha)*cos(gamma)+cos(alpha)*sin(beta)*sin(gamma) ]
    [ -cos(beta)*sin(gamma)                                 ]

つまり、r'_z=-cos(beta)*sin(gamma) が答えになります。これが正なら左に傾き、負なら右に傾いていることになります。

地面と平行な面との角度を知りたければsin^-1をとります。斜辺が長さ1のデバイスX軸正方向に沿ったベクトル、垂線がr'_zの三角形を思い浮かべれば分かると思います。

th = sin^-1(-cos(beta)*sin(gamma))

これだと画面の回転を考慮していません。デバイスの自然な向きで使用しているときは問題ありませんが、portraitやlandscapeといった画面の向きを変えるとおかしな事になります。

それを補正するために、screen.orientation.angleを使用します(もしこのプロパティが使えないならlockOrientationで画面の向きを固定するか、諦めて他の入力方法に切り替えた方が良いかもしれません)。

画面上の座標からデバイス座標へ変換するには、screen.orientation.angleだけZ軸まわりに逆回転させます。なので、画面上の座標から地球座標へ変換する式は次のようになります。

r' = matRotZ(alpha)*matRotX(beta)*matRotY(gamma)*matRotZ(-screen.orientation.angle)*r

先ほどと同じように行列を結合すると次のようになります。

angle = -screen.orientation.angle
r' = [ cos(alpha)*cos(gamma)*cos(angle) -sin(alpha)*( cos(beta)*sin(angle)+sin(beta)*sin(gamma)*cos(angle) ) ]
     [ sin(alpha)*cos(gamma)*cos(angle) +cos(alpha)*( cos(beta)*sin(angle)+sin(beta)*sin(gamma)*cos(angle) ) ]
     [ sin(beta)*sin(angle)-cos(beta)*sin(gamma)*cos(angle)                                                  ]

角度は次のようになります。

th = sin^-1(sin(beta)*sin(angle)-cos(beta)*sin(gamma)*cos(angle))

最終的なJavaScriptのコードは次のようになります。

function toRad(deg){return deg*(Math.PI/180);}
function toDeg(rad){return rad*(180/Math.PI);}
function getLeftTiltAngle(ev){
    // r = rotz(alpha)*rotx(beta)*roty(gamma)*rotz(-screen.orientation.angle)*colvec[1,0,0]
    // tilt = sin^-1(r.z)
    var b = toRad(ev.beta);
    var c = toRad(ev.gamma);
    var d = toRad(-screen.orientation.angle);
    var rz = Math.sin(b)*Math.sin(d)-Math.cos(b)*Math.sin(c)*Math.cos(d);
    return toDeg(Math.asin(rz));
}

以上の論法はDeviceOrientation Event SpecificationのWorked Exampleに書いてあるのとほとんど同じです。そちらはデバイス背面方向の方位を求める例ですね。

他にもdevicemotionイベントを使用して傾きを求める方法もあります。傾きを求めるだけならばコンパス(方位)の情報は不要なので、むしろこちらの方が本筋かもしれません。accelerationからaccelerationIncludingGravityを引くと重力加速度のベクトルが得られます。そのベクトルからデバイスX軸正方向の成分だけを取り出せば良いのです。もちろん画面の向きを考慮するなら、X軸正方向のベクトルをscreen.orientation.angleでZ軸回りに逆回転させて、そのベクトルと重力加速度のベクトルとの内積を求めます。JavaScriptのコードにすると次のようになります。

function getLeftTiltAngleFromMotionEvent(motionEvent){
  var gravityX = motionEvent.acceleration.x - motionEvent.accelerationIncludingGravity.x;
  var gravityY = motionEvent.acceleration.y - motionEvent.accelerationIncludingGravity.y;
  var gravityZ = motionEvent.acceleration.z - motionEvent.accelerationIncludingGravity.z;
  var screenAngleRev = toRad(-screen.orientation.angle);
  var rightX = Math.cos(screenAngleRev);
  var rightY = Math.sin(screenAngleRev);
  var dotGR = rightX * gravityX  + rightY * gravityY;
  var rightGravity = dotGR / Math.sqrt(gravityX*gravityX+gravityY*gravityY+gravityZ*gravityZ);
  return -(90 - toDeg(Math.acos(rightGravity)));
}

両方の方法を試してみましたが、deviceorientationを使った方が若干安定しているように見えます。devicemotionを使った方は、デバイスを振ったときに角度が大きく変化してしまいました。

以下、動作例。対応しているブラウザだと赤いバーが左右に動くはず。

2014-10-28

JavaScript DeviceOrientationでデバイスの向きや加速度を得る

JavaScriptのDeviceOrientationイベントについて調べたついでに、向きや加速度をグラフで表示するものを作りました。Android(Nexus5, Xperia Z2 Tablet)のChromeとFirefoxで動作を確認。

DeviceOrientationイベントでは、デバイスの標準的な画面の向きに対して右がX軸正方向、上がY軸正方向、手前がZ軸正方向という座標系をデバイス座標フレーム(Device coordinate frame)と称して、それを前提にした回転角や加速度が色々と得られます。

例えばwindowオブジェクトのdeviceorientationイベントが発生したとき、そのイベントオブジェクトのalpha, beta, gammaプロパティの値はそれぞれZ軸, X軸, Y軸まわりの回転角(度数単位)を表します。X軸正方向を東、Y軸正方向を北、Z軸正方向が天頂を指すような姿勢を基準として、そこから現在のデバイスの姿勢への回転角が取得できます。ただし、absoluteプロパティがfalseの時は、そのような絶対的な値ではない、何らかの近似値となるようです(詳細不明)。角度が得られない場合、各プロパティはnullになります。

加速度はdevicemotionイベントで得られます。accelerationプロパティとaccelerationIncludingGravityプロパティではデバイス座標フレームでの角軸方向の加速度(m/s^2)が得られます。accelerationは手に支えられて落下しないときに0、accelerationIncludingGravityは自由落下(無重力)の時に0(手で持っているときは天頂方向へ約9.8m/s^2加速)となります。(2014-12-16追記: AndroidとiOSとで軸の向きが逆になっているようです。右へ加速させたとき、Androidだとxは正になりますがiOSだと負になります。yもzもそれぞれ逆向きです)

devicemotionイベントのrotationRateプロパティでは角速度がdeg/sの単位で得られます。

devicemotionイベントのintervalプロパティでは加速度検出の分解能がミリ秒で得られるようです。ただ、Android版のFirefoxでは100msを返しておきながら、数ミリ秒(5ms前後)間隔というきわめて細かい単位でイベントが発生しており、どうなっているのかよく分かりません。イベント毎にグラフを更新したら重すぎてしかたがなかったので、ある程度間引く処理を入れました。

ちなみに、deviceorientationイベントの方は定期的にイベントが起きるわけではなく、姿勢に変化が起きたときか、または、新しいリスナーが登録されたときとのことです。

一番注意しなければならないのは、デバイス座標フレームはあくまでデバイスの姿勢を基準にしたものであって、画面の内容を基準にしたものでは無いという点です。つまり、画面の向き(screen.orientation)が縦(portrait)であろうが横(landscape)であろうが、得られる値はデバイスの標準的な向きを基準にした値のみが得られます。なので、画面の向きを基準にしたい場合は、何らかの補正処理が必要になります。

補正にはscreen.orientation.angleプロパティが使えそうですが、対応していないブラウザも多いようです。screen.orientation.typeでは4種類の向きの内いずれかが分かりますが、結局どれが標準的な向きなのかが分からないので困ります。向きをscreen.orientation.lock("natural")してしまえば良いのかもしれませんが、それで済むとも限らないですよね。どうすれば良いんでしょうね。

2014-10-22 ,

KB3000988でインストールが遅いのは解決せず

KB2918614でインストールが遅くなった件ですが、10/13にWindows Installerに関する新しい更新プログラム KB3000988 が提供されたので、問題が解決されたか確認しました。

「コンポーネント登録を更新しています」の段階で長時間待たされ、その間インストールする全てのファイルを読み込んでおり、その後、ファイルをコピーする段階で再度全てのファイルを読み込むという、二度読みする挙動はそのまま変わっていませんでした。遅い。遅すぎる。どうするんでしょうね、これ。