f8g

Function Map

Ctrl + クリック長押し、離したときに実行。場所で覚えるGUI

// ==UserScript==
// @name          Function Map
// @namespace     http://d.hatena.ne.jp/arikui/
// @include       *
// ==/UserScript==

var fields = [
    new Field(
        new Circle(new Point(window.innerWidth / 2, window.innerHeight / 2), window.innerHeight / 2),
        function(){
            alert("center");
        }
    ),
    new Field(
        new Triangle(new Point(0, window.innerHeight), new Point(300, window.innerHeight), new Point(0, window.innerHeight - 300)),
        function(){
            alert("left bottom");
        }
    ),
    new Field(
        new Rectangle(new Point(window.innerWidth / 2, 0), window.innerWidth / 2, window.innerHeight),
        function(){
            alert("right");
        }
    )
];

fields.canvas = null;

fields.execute = function(x, y, e){
    var point = new Point(x, y);
    this.forEach(function(field){
        if(field.geom.inclusion(point))
            field.exec(field, e);
    });
};

fields.createMap = function(){
    var canvas = fields.canvas || document.createElement("canvas");

    canvas.style.left    = "0px";
    canvas.style.top     = window.pageYOffset + "px";
    canvas.style.display = "block";

    if(!fields.canvas){
        canvas.width  = window.innerWidth;
        canvas.height = window.innerHeight;
        canvas.style.position = "absolute";

        canvas.addEventListener("mouseup", function(e){
            fields.execute(e.clientX, e.clientY, e);
        }, false);
    
        this.forEach(function(field){
            var color = field.color || Field.color(Math.random());
            field.geom.draw(canvas, color);
        });
    
        fields.canvas = canvas;
        document.body.appendChild(canvas);
    }

    return canvas;
};

/* Field */
function Field(geom, exec){
    this.geom = geom;
    this.exec = exec;
};

Field.color = function(value){
    value  = value * 359;
    var ht = value * 6;
    var d  = ht % 360;
    
    var t2 = function(){
        return Math.round(((255 - d) / 360 * 255) / 255 * 255);
    };

    var t3 = function(){
        return Math.round((255 - (255 - d) / 360 * 255) / 255 * 255);
    };

    switch(Math.round(ht / 360)){
        case 0 : var r = [255, t3(), 0]; break;
        case 1 : var r = [t2(), 255, 0]; break;
        case 2 : var r = [0, 255, t3()]; break;
        case 3 : var r = [0, t2(), 255]; break;
        case 4 : var r = [t3(), 0, 255]; break;
        default: var r = [255, 0, t2()]; break;
    }

    return "rgba(" + r + ",0.3)";
}

/* key events */
var isMousedown = false;

var onmousedown = function(e){
    if(!e.ctrlKey || isMousedown)
        return;

    var callback = arguments.callee;
    isMousedown = true;

    setTimeout(function(){
        if(!isMousedown)
            return;

        document.removeEventListener("mousedown", callback, false);

            fields.createMap();
    }, 1000);
};

document.addEventListener("mousedown", onmousedown, false);

document.addEventListener("mouseup", function(e){
    isMousedown = false;
    document.addEventListener("mousedown", onmousedown, false);

    if(fields.canvas)
        fields.canvas.style.display = "none";
}, false);

/* geometories */
// point
function Point(x, y){
    this.x = x;
    this.y = y;
}

// polygon
function Polygon(){
    switch(arguments.length){
        case 3: return Triangle.apply(null, arguments);
    }

    this.points = Array.prototype.slice.apply(arguments, null);
    this.center = new Point(0, 0);

    this.points.forEach(function(point){
        this.center.x += point.x;
        this.center.y += point.y;
    });

    this.center.x /= this.points.length;
    this.center.y /= this.points.length;
}

Polygon.prototype.draw = function(canvas, color){
    var context = canvas.getContext("2d");

    context.fillStyle = color;
    context.beginPath();

    this.points.forEach(function(p, i){
        if(i == 0)
            context.moveTo(p.x, p.y);
        else
            context.lineTo(p.x, p.y);
    });

    context.lineTo(this.points[0].x, this.points[0].y);
    context.closePath();
    context.fill();
}

// triangle
function Triangle(){
    this.points = Array.prototype.slice.apply(arguments, null);
}

Triangle.prototype.inclusion = function(point){
    var rs = [
        (this.points[1].x - this.points[0].x) * (this.points[2].y - this.points[0].y)
      - (this.points[1].y - this.points[0].y) * (this.points[2].x - this.points[0].x),
        (this.points[2].y - this.points[0].y) * (point.x - this.points[0].x)
      - (this.points[2].x - this.points[0].x) * (point.y - this.points[0].y),
        (this.points[1].x - this.points[0].x) * (point.y - this.points[0].y)
      - (this.points[1].y - this.points[0].y) * (point.x - this.points[0].x)
    ];

    return (0 < rs[1] && 0 < rs[2] && 0 < rs[0] - rs[1] - rs[2])
        || (0 > rs[1] && 0 > rs[2] && 0 > rs[0] - rs[1] - rs[2]);
};

Triangle.prototype.draw = Polygon.prototype.draw;

// rectangle
function Rectangle(p, width, height){
    this.width  = width;
    this.height = height;
    this.points = [
        p,
        new Point(p.x + width, p.y),
        new Point(p.x + width, p.y + height),
        new Point(p.x, p.y + height)
    ];
}

Rectangle.prototype.inclusion = function(point){
    return point.x > Math.min(this.points[0].x, this.points[2].x)
        && point.x < Math.max(this.points[0].x, this.points[2].x)
        && point.y > Math.min(this.points[0].y, this.points[2].y)
        && point.y < Math.max(this.points[0].y, this.points[2].y);
}

Rectangle.prototype.draw = function(canvas, color){
    var context = canvas.getContext("2d");
    context.fillStyle = color;
    context.fillRect(this.points[0].x, this.points[0].y, this.width, this.height);
};

// circle
function Circle(point, r){
    this.center = point;
    this.r = r;
}

Circle.prototype.inclusion = function(point){
    var d = Math.sqrt(Math.pow(this.center.x - point.x, 2) + Math.pow(this.center.y - point.y, 2));
    return d <= this.r;
};

Circle.prototype.draw = function(canvas, color){
    var context = canvas.getContext("2d");
    context.fillStyle = color;
    context.beginPath();
    context.arc(this.center.x, this.center.y, this.r, 0, 2 * Math.PI, 1);
    context.fill();
}

TomblooのexecuteWSHでオブジェクトのやりとり

TomblooのexecuteWSHはこういう仕様になってます。

/**
 * Windows上でWSHを実行する。
 * スクリプト内でWScript.echoなどで出力された文字列も返り値に含まれる。
 * 
 * @param {Function} func WSHスクリプト。
 * @param {Array} args WSHスクリプトの引数。 
 * @param {Boolean} async 非同期で実行するか。 
 * @return {String} WSHスクリプトの実行結果。
 */

第1引数でfunctionを渡し、渡したfunctionの引数にargsが入ってくる、という感じです。argsをWSH側に渡す際、それぞれの値をunevalしているので、objectでも渡せるという仕組み。つまり、WSHFirefoxJavaScript、どちらでも同じオブジェクトを共有しているような、スクリプトエンジンの境目を意識させないプログラミングが可能になってます。格好いいですね。
ただし、WSH側から返ってくる値は全て文字列ってのが悲しい。多少、無理矢理にでもWSH側から返ってくるオブジェクトをそのまま使えるようにしたい。

JSONライブラリをロードさせる

そもそもWSHJSONをサポートしてないのが問題なので、外からライブラリを持ってきて読み込ませます。
使う → http://www.json.org/js.html
TomblooではMochiKitがロードされてるのでそれを使おうかと思ったんですが、JSON関係だけを読み込ませるだけでもなかなか面倒そうだったんで、もっとシンプルなやつを選びました。ネイティブのprototypeに書き込みまくってるけど気にしない。

addAround(this, "executeWSH", function(proceed, args){
	// http://www.json.org/json2.js
	var jsonLib = (function () {if (!this.JSON) {JSON = {};}(function () { function f(n) {return n < 10 ? "0" + n : n;} if (typeof Date.prototype.toJSON !== "function") {Date.prototype.toJSON = function (key) {return this.getUTCFullYear() + "-" + f(this.getUTCMonth() + 1) + "-" + f(this.getUTCDate()) + "T" + f(this.getUTCHours()) + ":" + f(this.getUTCMinutes()) + ":" + f(this.getUTCSeconds()) + "Z";};String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) {return this.valueOf();};}var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = {'\b': "\\b", '\t': "\\t", '\n': "\\n", '\f': "\\f", '\r': "\\r", '"': "\\\"", '\\': "\\\\"}, rep; function quote(string) {escapable.lastIndex = 0;return escapable.test(string) ? "\"" + string.replace(escapable, function (a) {var c = meta[a];return typeof c === "string" ? c : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);}) + "\"" : "\"" + string + "\"";} function str(key, holder) {var i, k, v, length, mind = gap, partial, value = holder[key];if (value && typeof value === "object" && typeof value.toJSON === "function") {value = value.toJSON(key);}if (typeof rep === "function") {value = rep.call(holder, key, value);}switch (typeof value) {case "string":return quote(value);case "number":return isFinite(value) ? String(value) : "null";case "boolean":case "null":return String(value);case "object":if (!value) {return "null";}gap += indent;partial = [];if (Object.prototype.toString.apply(value) === "[object Array]") {length = value.length;for (i = 0; i < length; i += 1) {partial[i] = str(i, value) || "null";}v = partial.length === 0 ? "[]" : gap ? "[\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "]" : "[" + partial.join(",") + "]";gap = mind;return v;}if (rep && typeof rep === "object") {length = rep.length;for (i = 0; i < length; i += 1) {k = rep[i];if (typeof k === "string") {v = str(k, value);if (v) {partial.push(quote(k) + (gap ? ": " : ":") + v);}}}} else {for (k in value) {if (Object.hasOwnProperty.call(value, k)) {v = str(k, value);if (v) {partial.push(quote(k) + (gap ? ": " : ":") + v);}}}}v = partial.length === 0 ? "{}" : gap ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" : "{" + partial.join(",") + "}";gap = mind;return v;default:;}} if (typeof JSON.stringify !== "function") {JSON.stringify = function (value, replacer, space) {var i;gap = "";indent = "";if (typeof space === "number") {for (i = 0; i < space; i += 1) {indent += " ";}} else if (typeof space === "string") {indent = space;}rep = replacer;if (replacer && typeof replacer !== "function" && (typeof replacer !== "object" || typeof replacer.length !== "number")) {throw new Error("JSON.stringify");}return str("", {'': value});};}if (typeof JSON.parse !== "function") {JSON.parse = function (text, reviver) {var j; function walk(holder, key) {var k, v, value = holder[key];if (value && typeof value === "object") {for (k in value) {if (Object.hasOwnProperty.call(value, k)) {v = walk(value, k);if (v !== undefined) {value[k] = v;} else {delete value[k];}}}}return reviver.call(holder, key, value);} cx.lastIndex = 0;if (cx.test(text)) {text = text.replace(cx, function (a) {return "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);});}if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]").replace(/(?:^|:|,)(?:\s*\[)+/g, ""))) {j = eval("(" + text + ")");return typeof reviver === "function" ? walk({'': j}, "") : j;}throw new SyntaxError("JSON.parse");};}}());});

	var _args = [
		function(){
			try{
				var args = Array.apply(null, arguments);
				args.pop()();
				return JSON.stringify(args.pop().apply(null, args));
			}
			catch(e){
				return e.description;
			}
		},

		args[1].slice()
	];

	_args[1].push(args[0], jsonLib);

	var result = new Function("return " + proceed(_args))();

	return result;
});
var r = executeWSH(function(a, b, c){
	return {p: (a + b + c).toString()};
}, [1,2,3]);

log(r);

WSH.Echoを使ったりする時も、ちゃんとJSON形式を意識しないとダメとか制限が出てきますが、使い方を限定すれば使いやすくなるかも。

Dashboard Drag Scroll

SpaceキーやPageDownキーやLDRizeより細かい移動が欲しい人向け。Dashboardの左右両端をドラッグしてスクロールするようにします。(実際の移動量の7倍スクロール → position.rato(ratioのスペルミス))

// ==UserScript==
// @name          Dashboard Drag Scroll
// @namespace     http://d.hatena.ne.jp/arikui/
// @include       http://www.tumblr.com/dashboard
// ==/UserScript==

var position = {
	mutable: false,
	prev   : null,
	rato   : 7
};

document.body.addEventListener("mousedown", function(event){
	event.preventDefault();
	event.stopPropagation();

	var targetId = event.target.getAttribute("id");

	if(!targetId.match(/^(dashboard_index|container|content)$/))
		return;

	position.mutable = true;
	position.prev    = event.clientY;
}, false);

document.body.addEventListener("mouseup", function(event){
	event.preventDefault();
	event.stopPropagation();

	position.mutable = false;
}, false);

document.body.addEventListener("mousemove", function(event){
	event.preventDefault();
	event.stopPropagation();

	if(!position.mutable)
		return;

	window.scrollBy(0, - position.rato * (event.clientY - position.prev));
	position.prev = event.clientY;
}, false);

空間に余裕があるウェブページでは、その余った空間をユーザが自由に使うことができる、と考えると、何もない部分にどんな機能を付け足そうか、考えてるだけで楽しくなってきますね。

WSHでImageMagick、Tomblooに位置情報付加

ImageMagickはCOMからも使えるんですね。
Install the ImageMagickObject COM+ Component @ ImageMagick
たとえば、identify -verbose rose.jpgをWSHで実行したいときはこうやります。

var ImageMagick = WSH.CreateObject("ImageMagickObject.MagickImage.1");
ImageMagick.Identify("-verbose", "rose.jpg");

別にWshShellでいいじゃん、という気も。注意しないといけないのは、上のコードでは、ダダーッと情報が表示されるけれど、-formatのときなどはIdentifyが値を返すようになるところ。

var info = ImageMagick.Identify("-format", "%[fx:w/72] by %[fx:h/72] inches", "document.png");
WSH.Echo(info);

何だか使いづらい。

Tomblooで使うサンプル

executeWSHでImageMagickからExifを読み取って、逆ジオコーディングします。
使った → http://refits.cgk.affrc.go.jp/tsrv/jp/rgeocode.html

addAround(Tombloo.Service, 'post', function(proceed, args){
	var ps = args[0];

	if(ps.type !== "photo")
		return proceed(args);

	return succeed()
		.addCallback(function(){
			return ps.itemUrl;
		})
		// get coordinates by exif
		.addCallback(function(url){
			var exif = eval(executeWSH(function(){
				var ImageMagick = WSH.CreateObject("ImageMagickObject.MagickImage.1");
				return ImageMagick.Identify("-format",
				                            "[[%[EXIF:GPSLatitude]],[%[EXIF:GPSLongitude]]]",
				                            ARG_0);
			}, [url]));

			if(!exif[0].length)
				return null;

			return {
				lat: exif[0][0] + exif[0][1] / 60 + exif[0][1] / 3600,
				lon: exif[1][0] + exif[1][1] / 60 + exif[1][1] / 3600
			};
		})
		// get address
		.addCallback(function(coordinates){
			if(!coordinates)
				return proceed(args);

			var url = "http://refits.cgk.affrc.go.jp/tsrv/jp/rgeocode.php?" + queryString(coordinates) + "&jsonp";

			return loadJSONDoc(url).addCallback(function(res){
				if(!res.status)
					return proceed(args);

				var address = [
					res.prefecture.pname,
					res.municipality.mname,
					res.local.section
				].join("");

				ps.description = ps.description || "";
				ps.description += " (" + address + ")";

				return proceed(args);
			})
		})
});

動作例
http://arikui.tumblr.com/post/87195335/exif

executeWSHは引数の扱いがちょっと分かりづらい気もする。WshArgumentsで扱えた方が分かりやすいかなあ。あと、名前付き引数とかも(あまり使わないけど)。

Tomblooで自動タグ付加 その2

はてなブックマークからも取得、全ポストにタグ付け。いまいちだけどいいか。
del.icio.usMD5が面倒くさそう。

(function(){
	function Tag(init, args){
		this.tags = {};

		if(init && args)
			init.apply(this, args);
	}

	Tag.prototype = {
		add: function(tag, count){
			count = count || 1;
			tag = tag.toLowerCase();

			if(tag in this.tags)
				this.tags[tag] += count;
			else
				this.tags[tag] = count;

			return this.tags[tag];
		},

		toArray: function(len, threshold){
			var a = [{
				name    : name,
				count   : this.tags[name],
				toString: function(){
					return ("0000000000" + this.count.toString()).slice(-10);
				}
			} for(name in this.tags) if(typeof this.tags[name] == "number")];

			a.sort();
			a.reverse();

			if(len)
				a.length = len;

			if(threshold)
				a = [tag for each(tag in a) if(tag.count >= threshold)];

			return a;
		},

		toStrings: function(len, threshold){
			var a = this.toArray(len, threshold);
			return [tag.name for each(tag in a)];
		},

		merge: function(tag){
			if(tag) for(let name in tag.tags)
				this.add(name, tag.tags[name]);
			return this;
		}
	};

	models.LivedoorClip.getTags = function(itemUrl){
		var url = "http://api.clip.livedoor.com/json/comments?" + queryString({
			link: itemUrl,
			all : 1
		});

		return loadJSONDoc(url).addCallback(function(res){
			if(!res.isSuccess)
				return null;

			var init = function(comments){
				for each(let comment in comments) if(comment.tags){
					for each(let tag in comment.tags) if(typeof tag == "string")
						this.add(tag);
				}
			};

			return new Tag(init, [res.Comments]);
		});
	};

	models.HatenaBookmark.getTags = function(itemUrl){
		var url = "http://b.hatena.ne.jp/entry/json/?" + queryString({
			url: itemUrl
		});

		return loadJSONDoc(url).addCallback(function(res){
			if(!res)
				return null;

			var init = function(bookmarks){
				for each(let bookmark in bookmarks) if(bookmark.tags){
					for each(let tag in bookmark.tags) if(typeof tag == "string")
						this.add(tag);
				}
			};

			return new Tag(init, [res.bookmarks]);
		});
	}

	models.Delicious.getTags = function(itemUrl){
		var url = "http://feeds.delicious.com/v2/json/urlinfo/check?" + queryString({
			url: itemUrl
		});

		return loadJSONDoc(url).addCallback(function(res){
			if(!res)
				return null;

			var init = function(top_tags){
				this.tags = top_tags;
			};

			return new Tag(init, [res[0].top_tags]);
		});
	};
})();

addAround(Tombloo.Service, 'post', function(proceed, args){
	var use = {
		HatenaBookmark: true,
		LivedoorClip  : true,
		Delicious     : true
	};

	var ps = args[0];
	var res;

	for(let name in use) if(use[name]){
		if(!res){
			res = models[name].getTags(ps.itemUrl);
		}
		else{
			res.addCallback(function(t){
				if(!t)
					return models[name].getTags(ps.itemUrl);

				return models[name].getTags(ps.itemUrl).addCallback(function(tags){
					t.merge(tags);
					return t;
				});
			});
		}
	}

	if(!res)
		return proceed(args);

	return res.addCallback(function(tags){
		if(tags)
			ps.tags = tags.toStrings(3, 2);

		return proceed(args);
	});
});

修正

  • タグがないときのエラーを修正
  • 直した (明日)
  • 時にエラーなくポストされないことがあるみたいだけど、原因不明だから直してない
  • Deliciousを追加(さっき)

Tomblooで自動タグ付加

タグ付けとか面倒なわりにLivedoor Clipではタグからしか検索できないみたいで。他の人が付けたタグを見て自動でタグ付ける。

addAround(LivedoorClip, 'post', function(proceed, args){
	var ps  = args[0];
	var url = "http://api.clip.livedoor.com/json/comments?" + queryString({
			link: ps.itemUrl,
			all : 1
		});

	return loadJSONDoc(url).addCallback(function(res){
		if(!res.isSuccess)
			return proceed(args);

		res.Comments.getTags = function(len, threshold){
			var tags = {
				add: function(tag){
					if(tag in tags)
						tags[tag]++;
					else
						tags[tag] = 1;
				},

				toArray: function(){
					var a = [{
						name    : x,
						count   : this[x],
						toString: function(){
							return this.count.toString()
						}
					} for(x in this) if(typeof this[x] == "number")];

					a.sort();
					a.reverse();

					return a;
				}
			};

			for each(let x in this) if(x.tags){
				for each(let tag in x.tags)
					tags.add(tag)
			}

			var r = tags.toArray();

			if(len)
				r.length = len;

			if(threshold)
				r = [tag for each(tag in r) if(tag.count >= threshold)];

			return [tag.name for each(tag in r)];
		};

		ps.tags = res.Comments.getTags(3, 2);

		return proceed(args);
	});
});