滑らかな要素移動

画面の上から降りてくる通知ダイアログ部品のように、シングルページアプリケーションではDOM 要素の移動アニメーションを利用することが多く、近年の環境では CSS アニメーション、下位互換を考えた場合には jQuery といったライブラリを使うことかと思います。

http://hkitago.tumblr.com/post/166998101130


ページスクロールの参考コードを応用しバニラやピュアと呼ばれる手法で書き流用できるようにしました。

var moveTo = function(element, to, duration, speed, direction) {
  var rect = element.getBoundingClientRect()
  ,   difference = to - (direction === false ? rect.top : rect.left)
  ,   perTick = difference / duration * speed;
  setTimeout(function() {
    if(direction === false) {
      element.style.top = rect.top + perTick + 'px';
      if(parseFloat(element.style.top) === to) return;
    } else {
      element.style.left = rect.left + perTick + 'px';
      if(parseFloat(element.style.left) === to) return;
    }
    moveTo.call(this, element, to, duration - 10, speed, direction);
  }, 10);
};

検証

<div id="div"></div>
<div id="step"></div>
<style>
body { margin:0; padding:0; }
#div { position:absolute; z-index:0; width:100px; height:100px; background-color:yellow; border-radius:50%; }
#step { position:fixed; z-index:1; width:300px; height:300px; border-right:1px solid lightgray; border-bottom:1px solid lightgray; }
</style>
<script>
setTimeout(function(){
  moveTo.call(this, document.getElementById('div'), 300, 1000, 20, 1);
}, 1000);
</script>

余談ですが、アプリにライブラリを使うと著作権表示の実装が面倒ということもあってこちらを再確認。

参考

Googleマップのルート検索結果をカレンダーに追加するサービスメニュー

iOS 版の Google Maps アプリは電車やバスを使う場合のルート検索結果の詳細表示画面で、再下方にスクロールすると「カレンダーに追加」ボタンがありカレンダーアプリに予定を追加することができますが、MacOS のブラウザを使った場合に同じことができないものかと模索した結果、Automator でサービスメニューに追加するべくワークフローを作成することにしました。ファイルは Dropbox に置いたのでご自由にお使いください。

GMapルート検索結果をカレンダーに追加.workflow

使い方は、

  1. ダウンロード後、ZIP 解凍してダブルクリックをするとサービスインストーラーのダイアログが表示されるのでインストールボタンを押してください。
  2. ルート検索の詳細を表示した画面で、サービス>GMapルート検索結果をカレンダーに追加をメニューから選択してください。現在、Safari と Chrome のみ対応しています。

    電車/バスルート検索の詳細画面以外でサービスを実行するとエラーを返します。

  3. カレンダーアプリが前面に来て一覧を表示するので、予定を入れるカレンダー名を選択して OK を押してください。
  4. 予定の内容は、可能な限り iOS 版に近いものにしました。日跨ぎの予定に対応し、出発/到着時間の指定により一週間内で日付を判定して追加するようにしました。

出発/到着時間の設定にカレンダーで選択ができるにも関わらずその指定日へ予定が追加できないのは、グーグルが電車やバスの運行状況を曜日毎でのみデータを保持しているからなのか、検索結果に曜日しか表示されないという理由があります。AppleScript のソースコードは最初の手順で Automator で開くと見ることができるので、改変などご自由にどうぞ。

技術的な余談で言うと、Yosemite から利用できる JavaScript for Automation (JXA) での記述も検討したのですが、2つのブラウザの処理の違いとコード量、結局 JavaScript で DOM 操作をすることなどを考えると従来の方法になってしまいました。

参考:

AppleScriptでHTML解析

いよいよスプラトゥーン2の発売が迫り子供のお強請りが正に言葉通り凄まじくなって来たので、と言うのが今回の要件です。

実際にはこの初版運用から4回の販売があり、初めの3回は iPhone のメール通知の発見に遅れたためプッシュ通知が確実なメッセージアプリの利用に変更しました。4回目は通知が来たものの5分間隔では時既に遅しと言うことで、巡回の間隔を短くしました。

解析というほど大袈裟ではなく単に変更を確認するのは “SOLD OUT” ボタンタグの有無にし、混雑時のエラーページを回避する条件分岐を加えたそんなこんなで現在この様なコードで常駐しています。

property INTVAL_CHECK : 90
property INTVAL_NOTIFY : 300
property INTVAL : INTVAL_CHECK
property BASE_URL : "https://store.nintendo.co.jp/customize.html"
on idle
 try
  set rawHTML to do shell script "curl -si " & BASE_URL
  if paragraph 1 of rawHTML is "HTTP/1.1 200 OK" and rawHTML contains "カートに進む" then
   set INTVAL to INTVAL_NOTIFY
   my sendMessage("+8180********", "マイニンテンドーストアで Nintendo Switch の販売が開始されました。" & " " & BASE_URL)
   else
    set INTVAL to INTVAL_CHECK
  end if
 end try
 return INTVAL_CHECK
end idle
on sendMessage(targetBuddyPhone, targetMessage)
 tell application "Messages"
  set targetService to 1st service whose service type = iMessage
  set targetBuddy to buddy targetBuddyPhone of targetService
  send targetMessage to targetBuddy
 end tell
end sendMessage

当初 curl コマンドをオプション指定無しに利用していたところ、プログレスメータをアラートダイアログに表示するということがあり s オプションを、更に鯖落ちしていた場合に備え i オプションを付与しレスポンスヘッダが 200 の場合にのみ処理を実行する様にしました。また、メッセージアプリの宛先がメールアドレスにした場合のエラーを解決できなかったので仕方なく電話番号にしました。

自分からメッセージが送られるという特殊性を活かし、iOS のアドレス帳編集からメッセージ着信音を特別分かりやすいものにすると認知向上に尚良いかと思います。(因みにバルーンファイトのゲームオーバー音を設定しました)

最後に開発環境上で動作させることを考慮し Info.plist に <key>LSBackgroundOnly</key><true/> を追加することでドックにもアプリ切替時にもアイコンを表示させない様にしました。後は増産を待つばかり。

追記:
「0以外の状況でコマンドが終了しました。」というエラー内容(英語では “The command exited with a non-zero status”)は、シェルスクリプトの実行が失敗した際に 0 以外の数字を返した場合に発生することとその回避方法をアップルの公式文書で知りました。

http://hkitago.tumblr.com/post/162968916786

man curl を参照すると 1 から 99 までと XX を返す様で、シェルスクリプトのリダイレクトを利用すると空の値を返した際に次の行にある分岐条件が増えることが格好良く思わなかったので今回は Try ハンドラを利用しました。余談で公式文書にツッコミを入れると、リダイレクトを利用する場合は文末にアンパサンドが必要でないか、と…

追記1:
販売が開始されたら通知の間隔を大きくする目的で、2つの巡回用の数値変数を与えて条件分岐で変更できる様にしました。最初の通知を見逃すと痛手なのですが、気づいて買い物手続き中に通知だらけになるのも避けたいという難しいところ、リマインダアプリの「実行済み」ボタンの様な機能を通知持たせることができれば最善だと感じる苦肉の策。

追記2:
「文字列 “SOLD OUT” を含まない」と言う条件だとコード内でコメントアウトされていたり CSS で非表示になっていた場合に失敗することが分かったので修正。

参考:

WebViewのフォーム入力でImmersiveモードを維持する

前回、Javascript の alertconfirm 関数を使って WebView から AlertDialog を呼び出した際に全画面表示を止めてしまう問題について取り組んでいた時に、入力フィールドへフォーカスした際に Immersive モードが解除されるのは仕方がないとしても、その扱いについて考えていたところ、こちらも一癖ある挙動だったので紹介します。

通常 Immersive モードではない場合、WebView にキーボードが被るという問題を解決すべく、AndroidManifest.xml の activity ノード属性として android:windowSoftInputMode="adjustResize" と書くのですが、Immersive モードの場合はこの方法がうまくいきません。そこで試してみたのが、入力フィールドがフォーカスした時に Immersive モードを解除する、という方法です。

まず、MainActivity クラスの中に次のような JavaScript インターフェースを用意し、

public class WebViewJavaScriptInterface{
  private Context context;
  public WebViewJavaScriptInterface(Context context){
    this.context = context;
  }
  @JavascriptInterface
  public void disableFullScreen() {
    getWindow().getDecorView().setSystemUiVisibility(0);
  }
}

WebView について色々と書いてある onCreate の中で JavaScript から呼び出せるように設定しました。

webView.addJavascriptInterface(new WebViewJavaScriptInterface(this), "app");

そしてテキスト入力フィールドを1つ持っている HTML ではこのように JavaScript から Immersive モードを解除するようにしました。

document.querySelector('input[type=text]').ontouchend = function(){
  app.disableFullScreen();
};

ところが、Logcat に “Uncaught Error: Java exception was raised during method invocation” というエラーを出力しうまく動かなかったので、MainActivity で Handler を呼び出し、JavaScript インターフェース内の関数を次のようにすると期待通りの動作をエミュレータで確認しました。

@JavascriptInterface
public void disableFullScreen() {
  handler.postDelayed(new Runnable() {
    @Override
    public void run() {
      getWindow().getDecorView().setSystemUiVisibility(0);
    }
  }, 0);
}

また、JavaScript インターフェース内にもう一つ次のようなものを用意して(前回不要だと思った enableImmersiveMode 関数を再度用意)、

@JavascriptInterface
public void enableFullScreen() {
  handler.postDelayed(new Runnable() {
    @Override
    public void run() {
      enableImmersiveMode();
    }
  }, 0);
}

送信ボタンを押した時に Immersive モードとなるようにしました。

document.querySelector('#submit').ontouchend = function(){
  app.enableFullScreen();
};

実際にこのような挙動で動くようになったのですが、

ソフトウェアキーボード以外の場所で画面を押すと入力フィールドからフォーカスが外れてキーボードが隠れるという OS の設計上、このように加えておくと更に安心です。

document.querySelector('input[type=text]').onblur = function(){
  app.disableFullScreen();
};

実機で検証してみると、処理能力が劣る古い機種ではフルスクリーンになる際にステータスバーが途中で止まってしまったりということが確認できたので、postDelayed で遅延を750〜1000と与えると(動作的にはギクシャクする感は否めないのですが)解決しました。そして、最初に書いていた android:windowSoftInputMode="adjustResize" を外してみると…なんと入力フィールドが隠れずにキーボードに合わせて移動するという世にも奇妙な…。

参考:

WebViewのAlertDialogでImmersiveモードを維持する

Android の上部にあるツールバーと下部にあるナビゲーションバーを隠しフルスクリーンで表示する「Immersive モード」を使ってハイブリッドアプリを開発している際に、WebView 内の JavaScript で confirm を呼び出すと、全画面表示が解除されてツールバー及びナビゲーションバーが再び表示されてしまうという問題に直面したので調べました。

元々は confirm のボタンラベル名を拡張すべく WebChromeClient 内でこのようなコードを使っていました。

@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result)  {
  new AlertDialog.Builder(MainActivity.this)
    .setMessage(message)
    .setPositiveButton("確認",
      new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
          result.confirm();
        }
      })
    .setNegativeButton("消去",
      new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
          result.cancel();
        }
      })
    .setCancelable(false).create().show();
  return true;
};

この問題の根本にある原因はダイアログがフォーカスを持ってしまうことにあるようなので、参考にした文献のコードを利用し(ワンライナーフェチとしては少し残念な) AlertDialog を一旦 alertToShow に入れるようにして addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) を追加しました。

@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result)  {
  AlertDialog alertToShow = new AlertDialog.Builder(MainActivity.this)
(中略)
  alertToShow.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
  alertToShow.show();
  return true;
};

今回の失敗談を一つ紹介すると、MainActivity に次のような全画面設定関数を用意して setPositiveButtonsetNegativeButton のそれぞれの onClick で呼び出したのですが、案の定ボタンを押した後に Immersive モードを再開するという間抜けな結果になり、上述したコードで不要になりました汗。

@SuppressLint("NewApi")
private void enableImmersiveMode() {
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
    getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
  }
}

この例は JavaScript の confirm でしたが、もちろん alert でも同じような処理で解決できると思います。更に調べているとテキストフォーム入力時の input にフォーカスした際に下部からニュッと現れるソフトキーボードと共に Immersive モードが解除されるという問題について Stackoverflow で多く議論されていることが分かりました。こちらも原因は同様なのですが WebView と連携した場合にどうなるのか、別投稿で紹介したいと思います。

参考:

Safari 9.1.1でAppleScriptからJavaScriptを実行する

以前、Ingress のプレイ記録用途に AppleScript を書いたのだけど、半径3キロメートルに及ぶノバ作戦を近所で開催した時にもっと大きなインテルマップを表示しようと、次のようなコードを追加していました。

on fullScreenIntelMap()
	tell application "/Applications/Safari.app"
		tell document 1
			do JavaScript "document.getElementById('dashboard_container').style.left='0';document.getElementById('dashboard_container').style.top='0';document.getElementById('dashboard_container').style.right='0';document.getElementById('dashboard_container').style.bottom='0';document.getElementById('comm').style.display='none';document.getElementById('player_stats').style.display='none';document.getElementById('rs_box').style.display='none';"
		end tell
	end tell
end fullScreenIntelMap

この関数を起動時の on run と再読み込み時の on idle で実行すると、ヘッダ部分やフッタ部分を隠しブラウザのフルスクリーンモードにして可能な限り広域な地図状況を記録しておくことができるのですが、最近になってこのスクリプトを使うと Safari が次のようなエラーを返してきました。
スクリーンショット 2016-06-01 9.03.48

You must enable the ‘Allow JavaScript from Apple Events’ option in Safari’s Develop menu to use ‘do JavaScript’.

この内容に従って「開発」メニューを覗いてみると次のように「Apple Events からの JavaScript を許可」というコマンドがありました。

スクリーンショット 2016-06-01 9.04.24
この見慣れぬコマンドについて確認しようと Apple のサポートページを見ると未だ記述がないので、どうやら最新のバージョン 9.1.1 から追加されたものと思われます。そこでもう少しおググりなさってみると、数日前に “do JavaScript 記述のある AppleScript が動かなくなったよ〜” という内容で、とあるマックサイトのフォーラムに投稿している人がいました。

その内容によると、どうやら Safari の「開発」メニューを表示しておかないと上述したようなエラー内容を表示せず、コンソールに不可解なエラーメッセージだけが残るそうなので注意が必要です。

http://hkitago.tumblr.com/post/145244516611

確かに近年 JavaScript の台頭と安全性について語られることが増えていますが、わざわざ開発メニューから設定する必要があるのは柔軟性という点で開発者にとってはなかなか厳しい変更点なのではないかと思いました。またこのフォーラムでも懸念されていますが、Safari 以外で AppleScript と連携ができる(特にウェブ開発で使うような)アプリケーションの対応についても今後の動向に少し注目しておく必要がありそうです。

参考:

ポップアップするステータスバー表示を止める

Google Chrome ブラウザをフルスクリーンモードやプレゼンテーションモードで使用していると、リンクにマウスを乗せた際、画面左下にポップアップするステータスバーが表示されるのだけど、プレゼン資料用スライドショーアプリを使う場合など、これを表示しないようにする必要があり方法について検討しました。

スクリーンショット 2016-04-25 10.25.56

解決策は非常に簡単で、A タグに href 属性を与えないということでした。

window.addEventListener('load', function() {
	var aNodes = document.getElementsByTagName('a');
	for (var i = 0, max = aNodes.length; i < max; i++){
		aNodes[i].removeAttribute('href');
	}
}, false);

もちろん、マウスを乗せた場合に形状は変えたいという状況には、a:hover {cursor:pointer;} とスタイルを設定することで対応ができます。

元々のリンク遷移を生かしたい場合も、A ノードのループ処理内で aNodes[i].onclick = "location.href='" + aNodes[i].getAttribute('href') + "'"; と先に処理を入れると良いかと思います。フレーム処理している場合は target 属性の付与が必要になるのでご注意。

P.S. 最近は 直前に記述するので window.addEventListener が不要なことが増えました。

Android StudioでAPIレベルを変更する

Android アプリ案件では社内向けのような限られた端末を対象とした形で野良アプリを配布することが多く、雛形として複製したプロジェクトで API レベルを変更しなければいけない(特に Lollipop から Jelly Bean や KitKat へ)という状況が多いのですが、今回は Android Studio で簡単に処理する方法を Stack Overflow で見つけたので紹介します。

具体的には、build.gradle ファイルに記述されている minSdkVersiontargetSdkVersion の値を目的の API レベルに変更するとエディタの上部右肩に表示される “Sync Now” リンクをクリックしてからメニューコマンド「Build>Rebuild Project」を選択するだけです。

スクリーンショット 2016-02-04 17.10.46

build.gradle ファイルの場所は src/build.gradle と書かれていますが、こちらの環境では Android Studio 1.5 内の遷移で Project/app/build.gradle にありましたので、参考リンクにある内容を照らしながら環境に応じて編集すべき該当ファイルを検討されると良いかと思います。

参考:

Chromeのブックマークバーにショートカットを当てる機能拡張

OS X で Safari をメインブラウザとして使っていた理由の一つに、コマンド+数字というショートカットでブックマークバーの項目を呼び出せることがあったのだけど、次のバージョンの開発プレビュー版に更新してから暫くこのショートカットが Chrome のようにタブを呼び出すようになってしまったことに加え、SafariDAVClient が暴走するという正式リリース前にはよくある問題に直面し、更にグレードダウンもできないということから、既定のブラウザを Chrome にし、ブックマークバー(Safari では「お気に入りバー」という)に並ぶブックマークレットをショートカットで呼び出す機能拡張を自作することにした。

http://hkitago.tumblr.com/post/123624450876


(この機能設定はようやく v9 10601.1.50 Seed 5で実装された)

実際にマニュフェストファイル(manifest.json)と本体の JavaScript ファイル(background.js)はこのようなものを書いた。

{
    "name": "Bookmarks Bar Launcher",
    "version": "1",
    "manifest_version": 2,
    "permissions": [
    "bookmarks",
    "tabs",
    "<all_urls>"
  ],
    "background": {
        "scripts": ["background.js"],
        "persistent": false
  },
    "commands": {
        "bblCommand1": {
            "suggested_key": {
                "default": "Ctrl+1",
                "mac": "Command+1"
            },
            "description": "Item 1"
        },
        "bblCommand2": {
            "suggested_key": {
                "default": "Ctrl+2",
                "mac": "Command+2"
            },
            "description": "Item 2"
        },
        "bblCommand3": {
            "suggested_key": {
                "default": "Ctrl+3",
                "mac": "Command+3"
            },
            "description": "Item 3"
        },
        "bblCommand4": {
            "suggested_key": {
                "default": "Ctrl+4",
                "mac": "Command+4"
            },
            "description": "Item 4"
        }
    }
}
chrome.commands.onCommand.addListener(function(command) {
	var url;
	switch(command) {
		case 'bblCommand1': url = url_items[0]; break;
		case 'bblCommand2': url = url_items[1]; break;
		case 'bblCommand3': url = url_items[2]; break;
		case 'bblCommand4': url = url_items[3]; break;
	}
	if((/^javascript:/).test(url)) {
		url = url.replace(/javascript:/g, '');
		chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
			chrome.tabs.executeScript(tabs[0].id, {code: decodeURI(url)});
		});	
	} else {
		window.open(url);	
	}
});
var url_items = new Array();
chrome.bookmarks.getTree(process_bookmark);
function process_bookmark(bookmarks) {
	for(var i = 0; i < bookmarks.length; i++) {
		var bookmark = bookmarks[i];
		if(bookmark.url && (bookmark.url).indexOf('chrome://bookmarks/')) {
			url_items.push(bookmark.url);
		}
		if(bookmark.children) {
			process_bookmark(bookmark.children);
		}
	}
	url_items.splice(4);
}

参考したスタックオーバーフローで知ったのだけど、Google Chrome Extension では最大4つまでしかコマンドを上書きできないそうで、Safari では1〜9までのショートカットになるところを、今回は1〜4までとなってしまった。左手でのショートカットキー操作を考えるとそれでも良いかなという気もしたので、名前を変えた同様の機能拡張を2つ用意することは止めておいた。

また、頻繁に利用する4つ共にブックマークレットを使うことから単に window.open しないで chrome.tabs.executeScript と振り分けをするところが肝。更にブックマークバーの初期状態では先頭に Bookmark という項目があるのでそれをスキップする処理も入れた。今のところの問題は、ブラウザを最初に起動した時に限りこの機能を読み込むので、起動中ブックマークバーの項目を入れ替えた場合に順序の整合性が取れない点にあるのでご注意。

最後に、この機能拡張を利用するには適当な場所にフォルダを作成し2つのファイルを保存した後、Chrome の環境設定>機能拡張と進み、右上の「デベロッパーモード」にチェックを入れると表示される「パッケージ化されていない機能拡張を読み込む…」ボタンを押し、先のフォルダを選択する。
スクリーンショット 2015-08-20 10.19.17

ここまでやってみてうまく動かない場合は、機能拡張環境設定画面の最下方右にある「キーボードショートカット」というリンクをクリックすると、改めてお好みのショートカットを割り当てることができるので設定すると良い。
スクリーンショット 2015-08-20 10.20.50

面倒臭いからパッケージ化された .crx ファイルをよこせ!という方がいらしたら遠慮なくご連絡ください。

http://hkitago.tumblr.com/post/124450780756

P.S. ちなみにウィンドウズ版でも動作することを確認していますが、Safari 体験が少ないと思うので手厚いサポートは遠慮したいところです笑

参考:

定期的に再読みしてスクショを撮るAppleScript

少し暑くなってきて朝方のイングレス活動(通称イン活)が気持ち良くなってきたのですが、イン活用に使い始めたグーグルプラスを眺めていると、コントロールフィールド構築の時間経過をアニメーション GIF にして投稿している人がいて、帰宅後に慌ててインテルマップのスクリーンショットを撮るよりは効果的だろうと思い、その方法を考えました。

まず参考にした Reddit のコメントから、ブラウザのエクステンションと言われる機能拡張で定期的な再読み込みをしながら、i=1;while [1];do screencapture ~/Desktop/$i.png;let i++;sleep 30;done のようなシェルスクリプトを実行させる方法を試してみたのですが、省エネルギー設定の画面スリープにより期待通りに撮影できていなかったり、最後にプロセスをプロンプトから終了するのが面倒だったので、これらの問題をまとめて AppleScript のアプリケーションにしたコードがこちらになります。

property NOW : 0
property INTVAL_CAP : 55
property INTVAL_RELOAD : 3300
property FILENUM : 0
property thePath : ""
property thePID : ""
on run
	set thePath to (choose folder name with prompt "Where would you like to save your file?")
	do shell script "caffeinate -di &>/dev/null &"
	set thePID to (do shell script "ps ax | grep caffeinate | grep -v grep | awk '{print $1}'")
end run
on idle
	set FILENUM to FILENUM + 1
	do shell script "screencapture -mx -T1 " & quoted form of (POSIX path of thePath & FILENUM & ".png")
	if NOW mod INTVAL_RELOAD = 0 and NOW is not 0 then
		tell application "/Applications/Google Chrome.app"
			reload active tab of window 1
		end tell
	end if
	set NOW to NOW + INTVAL_CAP
	return INTVAL_CAP
end idle
on quit
	if thePID is not "" then do shell script ("kill -9 " & thePID)
	continue quit
end quit

インテルマップの表示には Google Chrome ブラウザを使い、撮影箇所を全画面で表示してから右上の Link アイコンを押して恒久リンクをアドレスバーにコピペして再度読み込んでおきます(この辺りも自動化したいところ)。その後、このアプリケーションを起動すると画像の保存先を尋ねられるので適当にデスクトップにフォルダを作るなりしてインテルマップを表示しておくと5分毎に再読み込みし、30秒毎にスクリーンショットを撮影し連番を付けた PNG 画像を先に指定したフォルダへ保存していきます。

帰宅後はこのアプリケーションを終了すると省エネルギー設定も元通りになります。省エネルギー設定を無効にする caffeinate コマンドは Mountain Lion 以降となりますので、このアプリケーションの動作条件もこれに沿うことになります。また、出かける前には画面の光度を低くして行くといいかもしれません。

連番の PNG ファイルから動きの無いものは削除し(ここが手作業なのは少し厄介笑)プレビューアプリを使ってアニメーション GIF にするとこうなります。

最後に、このスクリプトを Automator で使うにはイベントハンドラを一度だけ実行し終了してしまうので上手くいきません。ですので、スクリプトエディタにコピペして保存する際には、次のようにフォーマットを「アプリケーション」とし「ハンドラの実行後に終了しない」オプションを有効にしてください。更に、再読み込みの時間が短すぎるとアカウント停止になる可能性もあるのでご注意ください。

スクリーンショット 2015-05-13 12.28.45

近年流行りのクラウドソーシングでも定期的に画面のスクショを撮る時給計算アプリを起動して作業する必要があったり、何かと応用ができそうな気もします。アニメーション GIF の作成手順については次の投稿にしたいと思います。

更新(5/22):
先の公開時のリロード時間で数回試したところ、特にプレイ時間が1時間を超えるような場合に次のような “Oops! Something went wrong..” というエラーが表示され復旧まで半日を要することが起きました。コードを読むと60分で “Human presence not detected.” を表示し、1分で再描画しているようでしたので、再読みとスクリーン撮影の時間間隔の設定を長くするように変更しました。
スクリーンショット 2015-05-20 14.23.55

同一IPで且つ、等間隔の再読み込みでボット判定されていそうなので、INTVAL_RELOAD を3000〜3300くらいの間で乱数にすることも検討する必要がありそうです。

数回このエラーを経験した後に下記にリンクしたヘルプページを参照し、右上にある「お問い合わせ」から問題の内容を送信すると「公式のツール以外使うなよ、コノヤロー!」という趣旨の自動送信メールが返ってきて復旧しました。どうぞお気をつけください。

参考: