ラベル javascript の投稿を表示しています。 すべての投稿を表示
ラベル javascript の投稿を表示しています。 すべての投稿を表示

2016/02/21

[js]サイズに合わせた画像の縮小

このエントリーをはてなブックマークに追加

画像を任意のサイズに縮小するときの計算方法について説明します。
ソースコードにはJavascriptを使っていますが、なるべく言語に依存しないようにするつもりです。

この手の計算は、寄り付かない人には、ものすごく苦手意識があるのではないかと想像します。
落ち着けば、大変な計算はぜんぜん出てきません。
また、理屈を簡潔に語るよりも、計算から感覚的にイメージがつかめるほうがよいと思ったので、わりとくどくど説明しています。


画像を任意のサイズに合わせる


画像を任意の矩形サイズに合わせたいとします。
やってくる画像のサイズはまちまちですが、なるべく画像の全容が見えるようにして、収まらない部分は切り落としてしまうことにします。


当然ですが、幅と高さにサイズを指定して、強引にギュッとゆがめてしまうのは画像にもよろしくありません(変倍というやつです)。
幅と高さは同じ比率(等倍)で収めるものとします。


横長画像か縦長画像かを判定する


画像によって、処理の仕方は2パターンあります。
横長の画像は左右が余るようにしたいですし、縦長の画像は上下を余るようにしたいですね。

具体的な数をもとに考えることができるように、例として、フラミンゴの画像とヒマワリの画像を使います。
それぞれの画像サイズはこちら。
高さ
Aフラミンゴの画像450px300px
Bヒマワリの画像400px400px
描画先の矩形サイズ100px80px

フラミンゴの画像とヒマワリの画像を、描画先の矩形サイズに合わせて縮小したいとします。

それぞれの縦横の比率を比較することで、どちらに合わせる画像かを判別させます。

フラミンゴの縦横比率(幅/高さ)= 450px / 300px = 3/2 = 1.5
ヒマワリの縦横比率(幅/高さ)= 400px / 400px = 1.0
描画先の矩形の縦横比率(幅/高さ)= 100px / 80px = 5/4 = 1.2

縦横比率の関係は、
フラミンゴ(1.5)  > 描画先の矩形(1.2) > ヒマワリ(1.0)
となっています。

描画先の矩形よりも縦横比率が大きいものは横長として扱えばよく、小さいものは縦長として扱えばよいことが解りますね。


切り出す画像と縮小率を計算する


必要となるのは、画像のどの部分を切り出して、どのくらい縮小すればいいかという2つです。
ひとまず横長画像について考えます。


先に縮小比を求める
どのくらい小さく(または大きく)すればいいのかを考えます。

横長の画像は左右に余りますので、画像の高さを、描画先の矩形の高さに合わせると考えると解りやすいですね。

縮小比 = 描画先の矩形の高さ / 画像の高さ
です。

※縮小と書いていますが、計算式には縮小と拡大の区別はありません。画像が小さくて、描画先の矩形のほうが大きいときは、縮小比が1よりも大きくなるだけですね。

切り取り領域を決める
画像から領域を切り取り、縮小することで、描画先の矩形に合わせます。
どの部分の領域を切り取ればいいかを求めます。

・高さ
高さはそのまま、画像の高さを描画先の矩形の高さにするだけです。

切り取る領域の高さ = 画像の高さ

・幅
幅について考えます。
切り取る領域の幅に縮小比を掛ければ描画先の矩形の幅になるはずなので、切り出し幅を求めるためには逆に縮小比で割ってあげればよいでしょう。

描画先の矩形の幅 = 切り取る領域の幅×縮小比
なので、
切り取る領域の幅 = 描画先の矩形の幅/縮小比
で計算できます。

・切り取りの開始位置
また切り取りの開始位置は横幅の画像は左右に余る分を、少しずらせばいいことが判ります。
画像の幅と切り取り幅の差分があまりの部分ですので、真ん中の部分を切り取るためには、差分を等分すればよいですね。


ということで、
切り取りの開始位置 = (画像の幅 - 切り取り幅) / 2
で計算できます。


縦長の画像のときの計算

縦長の画像も同じように計算しましょう。
縦長の画像の場合は、縮小比は幅の比率になります。

縮小比 = 描画先の矩形の幅 / 画像の幅

・幅
切り取る領域の幅 = 画像の幅

・高さ
切り取る領域の高さ = 描画先の矩形の高さ/縮小比

・開始位置
開始位置は、上下の位置を決めます。
縦の切り取りの開始位置 = (画像の幅 - 切り取る高さ) / 2
で計算できます。


ソースコード

サンプルとして、画像をロードし、幅250px、高さ100pxに合わせて縮小するソースコードを載せておきます。
なお、言語はJavascriptです。
html内にcanvasのidを持つcanvasタグが配置されているものとします。

var imageFile = "/Images/ヒマワリ.jpg";
var id = "canvas"
var canvasWidth = 250;
var canvasHeight = 100;

var canvas = document.getElementById(id);
canvas.width = canvasWidth;
canvas.height = canvasHeight;
var context = canvas.getContext('2d');

var image = document.createElement('img');
image.src = imageFile;

// 準備
image.onload = function () {
    var imageWidth = image.width;
    var imageHeight = image.height;

    // ①縮小比を計算
    var scale = 1.0;
    if (imageWidth / imageHeight > canvasWidth / canvasHeight) {
        scale = canvasHeight / imageHeight;
    } else {
        scale = canvasWidth / imageWidth;
    }

    // ②切り出し領域を計算
    var clipWidth = canvasWidth / scale;
    var clipHeight = canvasHeight / scale;
    var clipX = (imageWidth - clipWidth) / 2;
    var clipY = (imageHeight - clipHeight) / 2;
  
    // ③drawImageメソッドの呼び出し
    context.drawImage(image, clipX, clipY, clipWidth, clipHeight, 0, 0, canvasWidth, canvasHeight);

};


解説

計算に使っている変数をざっとリストアップしておきます。
imageWidth画像のオリジナルサイズの幅
imageHeight画像のオリジナルサイズの高さ
canvasWidth指定矩形の幅
canvasHeight指定矩形の高さ
scale縮小比
clipX切り取り領域の開始X座標
clipY切り取り領域の開始Y座標
clipWidth切り取り領域の幅
clipHeight切り取り領域の高さ

準備として、画像を読み込んでから処理されるようにしたいのでonloadイベントに記述しています。


①縮小比を計算

縦横比率を比較し、横長の画像と、縦長の画像に分けて縮小比率を計算しています。


完全に横道にそれますが、割り算は掛け算に比べてものすごく遅いです。
(imageWidth / imageHeight > canvasWidth / canvasHeight)
は、下のようにも書けます。
(imageWidth * canvasHeight > imageHeight * canvasWidth)
こちらのほうが高速です。


②切り出し領域を計算

画像の切り出し領域を計算しています。
横長でも縦長でも縮小比が解れば同じ計算式で導くことができます。


ただし不要な演算が増えているので注意。


③drawImageメソッドの呼び出し
canvasのdrawImageメソッドを呼び出しています。
drawImageは、canvasにイメージを描画するためのメソッドです。
drawImageは引数の多い9つあるのを使っています。

context.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)


sx,sy,sw,shで描画元からコピーする矩形を指定し、dx,dy,dw,dhで描画先の矩形を指定します。


以上で終わりですが、もう一つだけ足しておきます。


おまけ


上記は、できるだけ画像を矩形いっぱいに広げて収めるという要件でした。
そのため矩形よりもはみ出た領域は切り取られてしまいます。

替わりに、画像を切り取ることなく、画像の全体を矩形に表示したいとします。
こういうのもありそうな話です。

こういう感じですね。

横長画像か縦長画像かを判定するために縦横比率を比較しました。
この判定を反転させます。

上のソースコードでいうと、
if (imageWidth / imageHeight > canvasWidth / canvasHeight) {

if (imageWidth / imageHeight < canvasWidth / canvasHeight) {
にする。
するとあら不思議。

幅と高さのどちらを基準にサイズを合わせるか、というのを逆転させればよいので、これだけでできるのです。


おしまい。

2011/07/31

[js][canvas]中抜けのパスを描画する

このエントリーをはてなブックマークに追加
canvasのパス描画の話です。

重ねたところを中抜きさせる方法です。
逆周りのパス書くと相殺されるようです。

●逆順のパスを描く



基準となるパスと、逆周りのパスを重ねると、重複したところが抜けます。arcメソッドの最後の引数はtrueとfalseでパスの向きが変わるような仕様でした。こうやって使うためのものなのか、というのが解った。

// 中空きのパスなら問題なく描画できる
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(100, 100, 100, 0, Math.PI * 2, true);
ctx.arc(100, 100, 80, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.arc(200, 100, 100, 0, Math.PI * 2, true);
ctx.arc(200, 100, 80, 0, Math.PI * 2, false);
ctx.fill();





円と、逆回転のパスで描いた円を僅かにスライドさせて重ねると、排他的論理和のベン図のような図形ができる。なるほど、ここまではいい。
だけどこうなると三種類の円での排他的論理和のベン図は無理そうです。
当然、上記の円を三つ重ねても望みどおりの図形はできない。

うーむ。
やっぱりまだ納得がいっていない。
完全に、すっぽり中空の図形だったら問題はないのです。だけど中が抜ける図形であることが描画前にわかってない場合は、内包判定しないといけなくて、これは限りなく面倒でしょう。


●globalCompositeOperationを使う方法


globalCompositeOperationを使うことでも、似たようなことはできます。

globalCompositeOperation="source-out"とかやれば、すでに描画されてるところを背景色で塗りつぶすように指定ができるわけです。

Canvasリファレンス - globalCompositeOperation = "合成方法"
http://www.htmq.com/canvas/globalCompositeOperation.shtml

感じとしては、前者の方法(逆順のパスを描く方法)のほうが、塗りつぶしの描画が一回で住む分、きっと高速でしょう。

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// source-overで上書き(初期値)
// source-out(xor)で排他描画
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(100, 100, 100, 0, Math.PI * 2, true);
ctx.fill();
ctx.globalCompositeOperation = "source-out";
ctx.beginPath();
ctx.arc(100, 100, 80, 0, Math.PI * 2, true);
ctx.fill();
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.arc(200, 100, 100, 0, Math.PI * 2, true);
ctx.fill();
ctx.globalCompositeOperation = "source-out";
ctx.beginPath();
ctx.arc(200, 100, 80, 0, Math.PI * 2, true);
ctx.fill();




もちろん、描画されているかどうかで判断されるので背景が描画されてたら使えないのです。
これも条件が厳しすぎて、まあ使い勝手がいいとは言えない。


●fill("everodd")を使う方法

追記:20161017

久しぶりにcanvasをいじってたら、どうやら塗りつぶしのタイミングで奇遇規則を指定できるようなので補足しておきます。
この記事の公開当時からこの設定があったかどうかは不明です。
ctx.fill("everodd");
fillを呼ぶタイミングで、evenoddを指定すると偶数奇数ルール(奇偶規則)が設定されます。
なおnonzeroを指定すると、省略時と同じ非ゼロ回転数ルールとなります。

うーん、こんなに簡単に…。

http://stackoverflow.com/questions/12446790/using-even-odd-fill-on-html-canvas

2011/06/11

[javascript]子ノードを全探査する

このエントリーをはてなブックマークに追加
子ノードを取得するのはchildNodesで一発ですが、子供の子供までひっくるめて、全部のノード探索するコードを考える。

root
-node1
--node1-1
--node1-2
---node1-2-1
-node2
--node2-1

こんな感じのノードを、この順番のままにリストアップする(rootはいらない)という命題。順番が味噌です。

●再帰を使わないノード探査


最初、再帰しないで書いてみようと思った。すぐに再帰に頼りがちだけど、ループでもこんなの余裕だろうと。これが、なんとも意味なかった。
利点がないのでした。きっと、木構造の探査にありがちな、末端から上に見ていく命題にしておけばもっと簡単だったかもしれません。
コードを乗せておきます。
// 再帰しないノード探査
function searchNodesLoop(root)
{
var list = [];
var stack = [];
var node = root.firstChild;
while (node != null)
{
// まず自分を処理
list.push(node);
// 子供の有無をチェック
if (node.firstChild != null)
{
// 自身に次のノードがあればスタックに積む(戻ってこれなくなるから)
if (node.nextSibling != null) {
stack.push(node.nextSibling);
}
node = node.firstChild;
}
else if (node.nextSibling != null) {
// 自分の次のノードの有無を確認
node = node.nextSibling;
}
else {
// どんづまりの場合はスタックから呼ぶ
node = stack.pop();
}
}
return list;
}


結局、小一時間くらい、あれこれやった。

●再帰を使ったノード探査


なんだかなあと釈然としなかったので、再帰するのも書いてみた。

// 再帰を使ったノード探査
function searchNodes(root)
{
var list = [];
var search = function (node)
{
while (node != null)
{
// 自分を処理
list.push(node);
// 子供を再帰
search(node.firstChild);
// 次のノードを探査
node = node.nextSibling;
}
}
search(root.firstChild);
return list;
}


めっちゃ簡単に書けた。javascriptならではなところがいくつかありますが、構造がシンプルなので、それもまたよいのではないかと。

あえて言えば、searchNodes関数内でローカルに作られるsearch関数は、関数が呼ばれるたびに作成されてしまう。これは無駄なので、クロージャーを駆使して改善しておくとよいかもしれません。この辺は可読性との兼ね合いですか。

まあこれで十分。
当然のことだけれど、再帰使わないのがよいというわけではないですね。

2011/01/09

[javascript]指定の要素を消すブックマークレット

このエントリーをはてなブックマークに追加
jQueryベースで閲覧しているページから指定の要素を消すブックマークレットを書きました。

ブックマークレットとは、お気に入り(ブックマーク)に、URLではなく動作するようにjavascriptなどのコードを登録しておき、必要に応じて呼び出すものを言います。改行などが混じっていると正常に動作しませんので注意。

ブックマークレット
ブックマークレット - Wikipedia
ブックマークレットとは - はてなキーワード

あんまりないですが、技術ドキュメントとかをプリント出力するさいなんかに、余分な要素(サイドのメニューとか)を非表示にしておけます。文書構造がしっかりしているページで利用できるととても強力です。

javascript:(function(){if(typeof jQuery=='undefined'){var jq=document.createElement('script');jq.type='text/javascript';jq.onload=bookmarklet;jq.src='http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';document.body.appendChild(jq);}$('#sidebar-wrapper').hide();})();


展開すると下のようになります。
javascript : (function ()
{
if (typeof jQuery == 'undefined')
{
var jq = document.createElement('script');
jq.type = 'text/javascript';
jq.onload = bookmarklet;
jq.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';
document.body.appendChild(jq);
}
$('#sidebar-wrapper').hide();
})();

ほとんどjQueryの呼び出しのためのコードです。
最後の$('#sidebar-wrapper').hide();を、必要に応じた選択方法と動作に変更してあげればよいでしょう。
ベースをjQueryとしているので、こういう処理はいくらでも応用できそうです。

指定のクラス/IDを消去する
$('#sidebar-wrapper').hide();
javascript:(function(){if(typeof jQuery=='undefined'){var jq=document.createElement('script');jq.type='text/javascript';jq.onload=bookmarklet;jq.src='http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';document.body.appendChild(jq);}$('#sidebar-wrapper').hide();})();


指定のクラス/IDに色をつける
$('.post-title .entry-title').css("background-color","lightblue");
javascript:(function(){if(typeof jQuery=='undefined'){var jq=document.createElement('script');jq.type='text/javascript';jq.onload=bookmarklet;jq.src='http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';document.body.appendChild(jq);}$('.post-title .entry-title').css("background-color","lightblue");})();


リンク部分に色をつける
$('a').css("background-color","yellow");
javascript:(function(){if(typeof jQuery=='undefined'){var jq=document.createElement('script');jq.type='text/javascript';jq.onload=bookmarklet;jq.src='http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';document.body.appendChild(jq);}$('a').css("background-color","yellow");})();


リンク部分を消す
$('a').contents().unwrap();
javascript:(function(){if(typeof jQuery=='undefined'){var jq=document.createElement('script');jq.type='text/javascript';jq.onload=bookmarklet;jq.src='http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';document.body.appendChild(jq);}$('a').contents().unwrap();})();


などなど。
ダイアログを出して呼び出しの都度、手動でセレクタを指定できるようにするには下記のようにします。

ダイアログで指定要素を色づけ
var sel=window.prompt("セレクタを指定してください", "");
$(sel).css("background-color","lightgreen");
javascript:(function(){if(typeof jQuery=='undefined'){var jq=document.createElement('script');jq.type='text/javascript';jq.onload=bookmarklet;jq.src='http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';document.body.appendChild(jq);}else{bookmarklet();}function bookmarklet(){var sel=window.prompt("セレクタを指定してください", "");$(sel).css("background-color","lightgreen");}})();

2010/11/05

javascriptでストップウォッチを実装する

このエントリーをはてなブックマークに追加
処理速度なんかを計るときに利用していたコードです。

javascriptによる処理時間を計測



処理時間を計測するときは下のようなコードになるかと思われます。
var st = new Date(); // 処理開始。

// 処理

var et = new Date(); // 処理終わり。
alert(et - st); // 経過時間をミリ秒で表示。


何がダメってわけでもないです。やってることは同じなので。
それがこう書ける。

var sw = new Stopwatch();
sw.start(); // 処理開始。

// 処理

sw.pause(); // 処理終わり。
alert(sw.getStamp()); // 経過時間をミリ秒で表示。


まあ、長くなってるような気がしないでもない。何がメリットかって、改めて見ると別にメリットなんてないですね。どうやっても処理時間なんてなんでも計れるんだし。まあいいや。
このストップウォッチは、C#のStopWatchクラスを模してます。中断ができる点が工夫で、あと整形して表示できる。つまり、一回計測を止めて、続きから計測を再開するようなコードが容易く書けるわけです。

var sw = new Stopwatch();
sw.start(); // 処理1開始。

// 処理1

sw.pause(); // 処理1終わり。

alert("ここの待機時間は計測されない");

sw.start(); // 処理2開始。
// 処理2
sw.pause(); // 処理2終わり。
alert(sw.getStamp()); // 処理1+処理2の経過時間をミリ秒で表示。


ストップウォッチ(コード)


やってることはすごくシンプルなので、コードも短いです。

// ■ StopWatch
function StopWatch()
{
var self = this;
var stamp = 0;
var startTime = null;
self.start = function ()
{
startTime = new Date();
};
self.pause = function ()
{
stamp = self.getStamp();
startTime = null;
};
self.reset = function ()
{
stamp = 0;
startTime = null;
};
self.getStamp = function ()
{
if (startTime == null) {
return stamp;
}
return new Date() - startTime + stamp;
};
self.getStampString = function ()
{
var val = self.getStamp();
// hh:MM:ss.SSSに整形(hが多いとき、SSSがないときは桁がずれる)
var s = (val % (60 * 1000)) / 1000;
var m = ((val / (60 * 1000)) | 0) % 60;
var h = ((val / (60 * 60 * 1000)) | 0) % 60;
if (s < 10) {
s = "0" + s;
}
if (m < 10) {
m = "0" + m;
}
if (h < 10) {
h = "0" + h;
}
return "" + h + ":" + m + ":" + s;
};
}


・メソッド




start計測を始めます。
pause計測を一旦止めます。
reset計測をリセットします。
getStamp経過時間をミリ秒単位で返します。
getStampString経過時間を整形して返します。

※整形については、コード見ればお分かりの通り、なんちゃってです。桁固定は保証していません。しかしデバックするだけならこれで十分。

・使用方法
/* ストップウォッチの使用方法 */
var sw = new StopWatch();
sw.start();
alert("計測中");
sw.pause();
alert("待機してた時間 " + sw.getStampString());

2010/10/20

円充填(circle-packing)

このエントリーをはてなブックマークに追加
円充填という概念があることを知ったので、遊んでました。コードなし、画像だけです。

その1

その2


真面目に勉強もせず、だらだらと自前で適当にコーディングを進めすぎたせいで、ぜんぜん納得いかず。かといって、汎用的にもなりそうにないし、処理には時間食うし。何かできそうだけれど、このまま続けても消耗モードになりそうなので、一時離れるつもりでここに書いておきます。何か思いついたらまた触る。

綺麗に隙間なく並べるにはどうやったらいいんだろう。どうしても不自然な隙間が連なってしまうんだなー。

元画像


円充填
Circle packing theorem

2010/10/03

[javascript]canvasでフィルタ処理

このエントリーをはてなブックマークに追加
canvasのgetImageDataとpuImageDataを駆使して非線形の座標変換による変形処理をしました。

・ねじる


・ふくらむ


・裏返る


・波立つ



懸念はやっぱり処理速度で、どの程度の実用できるレベルなのかというところ。
デモはこちらからです。

なお、まともに見れるのはGoogleChromeかSafariくらいなのでご注意を。FireFox3.6でも辛いです。
ブラウザベースでそこそこ遊べるので、面白い玩具を見つけたなって感じです。

2010/09/22

[javascript]連想配列の要素の削除

このエントリーをはてなブックマークに追加
連想配列の要素の削除について、誤解しやすいポイントを書き留めておきます。

deleteを使う

obj[a] = null;
null指定だと、obj.aというプロパティがnullの値を持つとして、残ってしまいます。
残っていると、for~inに引っかかるため、都合が悪いことが起こる可能性があるわけです。

連想配列の要素の削除には、deleteを使用しましょう。
delete obj[a];

すべての要素を消す

すべての要素を消すためにはfor~inで回してあげればよいです。
var obj = {a:1,b:2,c:3,d:4};
for(var key in obj){
    delete obj[key];
}
配列が対象だと全体の配列数が変化してしまうので、注意しないといけません。
けれど、連想配列は平気のようです。ちゃんと消えます。

連想配列についての補足

オブジェクトというのはもれなく連想配列です。オブジェクトのプロパティを利用した構造を連想配列と呼んでいるのです。
obj["a"]という書き方は、obj.aと書いても同じです。

面白いのはobj["a.b"]と書くこともできることです(改行を入れることもできる)。
これはもちろん、obj["a.b"]とobj.a.bは意味が異なるので注意。
/* これでも余裕で動く */
var obj = {};
obj["\n"] = 1; 
alert(obj["\n"]); /* 1が表示されます */

2010/09/15

[javascript]canvasのgetImageData関数でSECURITY_ERRにはまる

このエントリーをはてなブックマークに追加
●getImageDataについてはこちら
createImageData, getImageData, putImageData メソッド - Canvasリファレンス - HTML5.JP
http://www.html5.jp/canvas/ref/method/getImageData.html

●解決にはここが参考になりました
canvas の getImageDataが少しめんどくさい(特にローカルで動かす場合) - 地平線に行く
http://d.hatena.ne.jp/chiheisen/20100815/1281885412


「SECURITY_ERR: DOM Exception 18」というメッセージはChromeのもので、FireFoxだと「Security error code: 1000」というメッセージがでるようです。

要するに、getImageData関数は、外部から引っ張ってきた画像に対しては利用できない決まりになってて、ローカルで実行させると外部画像扱いになってしまうみたいです。

んなもん、言ってくれないと解んないよねー。
一回はまって調べたはずなのに、そのときはあっさりスルーしてたもんで、また時間使っちまったよ。

2010/05/31

Canvasでグラフィックパスの描画

このエントリーをはてなブックマークに追加


グラフィックパスの描画って、要するに直線と曲線のパスが複合的に重なり合った図形です。
パスが自己交差する場合に、内部に囲まれた図形の塗りつぶしを回避する必要があるわけです(いわゆるワインディング規則、奇遇規則とかいうやつです)。
だけど、canvasにはそんな仕様はない模様。
グラフィックパスは線画しかできないのかあ。これはショック。

2010/04/06

[javascript]canvas、drawImageの機能の違いによる速度の影響を調べる

このエントリーをはてなブックマークに追加
drawImageは、canvasで画像を使用するためのメソッドです。
パラメータがいろいろあるので、機能の違いから、速度にどのくらい影響するのかを調べてみました。

●drawImageの使い方


使い方は、下記を参照してください。
解りやすい解説があります。

Mozilla Developer Center > Canvas チュートリアル > 画像を使う
https://developer.mozilla.org/ja/Canvas_tutorial/Using_images


一度画像をImageオブジェクトに読み込ませないといけないところは他のグラフィック要素と違って面倒ではありますよね。

●処理時間の計測方法


ベースとなるサイズを320px×480pxとし、50%、100%(原寸)、150%、200%にした画像を用意する。
用意した画像について、下記の4パターンをテストしてみます。
  • ①same:画像を伸縮させて320px×480pxに収まるように配置する

  • ②half:画像を伸縮させて160px×240pxに収まるように配置する

  • ③pick:画像を50%切り出したものを伸縮させて、320px×480pxに収まるように配置する

  • ④fixed:画像を320px×480px切り出したものを、320px×480pxに収まるように配置する


上記それぞれの処理を100回繰り返し使用した時間を計測しました。なお、画像の読み込み時間は処理時間には含みません。
パラメータ次第でいろいろな結果がでそうではあるし、ローカル環境のスペックに依存するので数値に信頼性はありません。ざっくり傾向が分析できればよいぐらいの気持ちでお願いします。

●結果


Y軸の単位はmsです。
ChromeとFireFoxで行いました。


極端な結果になりました。しめたものです。
突出しているところは、処理が重いことを示しています。
なお、グラフになる前の数値データは、テストコードと一緒に後に載せます。

●考察


考察します。
結果から言えること。
  • Chromeは画像の一部を抜粋して伸縮する処理(③pickの処理)がネック。抜粋だけで、伸縮がなければ速度は十分出ている。Chromeの③の結果がなければ、全体的にFFよりも圧倒しています。

  • 小さい画像を扱うほうが処理速度は速い。まあ当然。ただし、Chromeに限っては、あまり倍率の異なる伸縮処理は重くなる。

  • FFは、伸縮処理が加わるとと途端に重くなる。処理速度は元の画像サイズに関わらず、配置後の画像サイズに比例している。

  • 共通していえるのは、伸縮処理は重いということ。元のサイズと、配置サイズを一致させるだけで、処理速度は全然違う。

  • 一番速度がでていたのは、Chromeにて実寸を50%のサイズにして配置する処理(100%の②halfの処理)。100回繰り返しで3ms。


ぐりぐりと触れて、再描画を繰り返さないといけない処理は、伸縮機能を使用しないように工夫するべきだという結果に。

●テストコード


function onLoad()
{
var container = document.getElementById("container");
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var srcList = ["img_050.png", "img_100.png", "img_150.png", "img_200.png"];
var funcList = [ function (img)
{
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 320, 480);
},
function (img)
{
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 160, 240);
},
function (img)
{
ctx.drawImage(img, 0, 0, img.width * 0.5, img.height * 0.5, 0, 0, 320, 480);
},
function (img)
{
if (img.width >= 320 && img.height >= 480) {
ctx.drawImage(img, 0, 0, 320, 480, 0, 0, 320, 480);
}
} ];
// 画像を全部読み込む
var imageList = [];
var loadImage = function (src)
{
var img = document.createElement("img");
img.onload = function ()
{
imageList.push(img);
// next
if (srcList.length > 0) {
loadImage(srcList.shift());
}
else {
loadedImage();
}
};
img.src = src;
img.alt = src;
};
loadImage(srcList.shift());
// 画像が全て読み込まれた後に呼び出される
var loadedImage = function ()
{
for (var i = 0; i < funcList.length; i++)
{
var func = funcList[i];
document.write("<p>");
document.write("<b>" + func + "</b><br>");
for (var j = 0; j < imageList.length; j++) {
var img = imageList[j];
test(img, func);
}
document.write("</p>");
}
};
var test = function (img, func)
{
var now = new Date();
for (var i = 0; i < 100; i++) {
func(img);
}
var stamp = new Date() - now;
document.write(img.alt + "=" + stamp + "<br>");
};
}


処理結果。
・Google Chrome4.1
function (img){ ctx.drawImage(img,0,0,img.width,img.height,0,0,320,480); }
img_050.png=25
img_100.png=18
img_150.png=38
img_200.png=50

function (img){ ctx.drawImage(img,0,0,img.width,img.height,0,0,160,240); }
img_050.png=5
img_100.png=3
img_150.png=21
img_200.png=32

function (img){ ctx.drawImage(img,0,0,img.width * 0.5,img.height * 0.5,0,0,320,480); }
img_050.png=271
img_100.png=1067
img_150.png=1491
img_200.png=18

function (img){ if(img.width >= 320 && img.height >=480){ ctx.drawImage(img,0,0,320,480,0,0,320,480); } }
img_050.png=0
img_100.png=19
img_150.png=18
img_200.png=18

・FireFox3.6
function (img) { ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 320, 480); }
img_050.png=633
img_100.png=20
img_150.png=639
img_200.png=650

function (img) { ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 160, 240); }
img_050.png=13
img_100.png=192
img_150.png=191
img_200.png=191

function (img) { ctx.drawImage(img, 0, 0, img.width * 0.5, img.height * 0.5, 0, 0, 320, 480); }
img_050.png=632
img_100.png=642
img_150.png=643
img_200.png=20

function (img) { if (img.width >= 320 && img.height >= 480) { ctx.drawImage(img, 0, 0, 320, 480, 0, 0, 320, 480); } }
img_050.png=0
img_100.png=20
img_150.png=20
img_200.png=20

2010/04/02

[javascript]true or false

このエントリーをはてなブックマークに追加
// ①
if (value) {
// 処理
}

// ②
if (value == true) {
// 処理
}

ある日ふと、上記①②の結果が異なることに気づく。
勝手にキャストされるんでしょ?と横柄にも思っていたけれど、どうもそうではないらしい。
当たり前だけど、数値⇔文字が自由に行き来するからといって、全ての型がそういうわけでもないのです。

どういう挙動なのだろうと不信に思ってしまったので、いろんな型の判定について調べてみる。
対象とするのは、undefined、null、数値(1、123、0、NaN)、文字("abc"、"1"、"123"、"0"、"")、日付、論理値(true、false)、オブジェクト型、配列、関数です。

テスト結果。


まとめ


  • 前述の通り、if(value)と(value==true)は結果が異なる。一致させるためには、Boolean(value)を使って明示的にキャストする必要がある。

  • if(value)がfalseになるのは、null、undefined、0、NaN、""、falseのとき。これは基本。

  • テスト内容とは関係ないけれどtypeof(null)はobjectになる。nullはobject。

  • (value == null)がtrueになるのは、null、undefinedのみ。

  • (value == true)と(value == 1)が一緒。

  • (value == false)と(value == 0)が一緒。(value == "")はvalue="0"のとき以外は一緒。

  • (value == true)と(value != false)は一致するとは限らない。true/falseと1/0などの論理値以外はあやしい。

  • 文字列の"true"は、Booleanとして自動的にキャストされたりしない。通常の文字列と同じ扱い。

  • 文字列の"0"は、数値にキャストされて、(value==false)と(value==0)にtrueを返す。(この辺がややこしい)

  • 配列が空かどうかが(value == false)で判別できる。if(value)は配列数に限らずtrueになる。

  • isNaNの挙動もよくわからん。objectや関数でtrueになるのはなんだろう。


論理値の挙動については、いまいち感覚的に理解しにくい印象です。
型の指定がないのをいいことに、混在させまくる利用は、コードが煩雑になることが避けられない気がします。やっぱりきっちり管理したほうが楽かも。

テストコード


以下、テストに使ったコードのベース。
実際はtrueになったところに色をつける処理を足しています。
var undef;
// undefined;
var list =
{
_undefined : undef, _null : null, _int_1 : 1, _int_123 : 123, _int_0 : 0, _nan : Number.NaN, _str_abc : "abc",
_str_1 : "1", _str_123 : "123", _str_0 : "0", _str_null : "", _str_true : "true", _bool_true : true,
_bool_false : false, _obj : {}, _array_null : [], _array_1 : [1], _func : function () {}
}
document.write("<table>");
document.write("<tr>");
document.write("<th>key</th>");
document.write("<th>typeof(value)</th>");
document.write("<th>value</th>");
document.write("<th>if(value)</th>");
document.write("<th>Boolean(value)</th>");
document.write("<th>value==true</th>");
document.write("<th>value==false</th>");
document.write("<th>value==1</th>");
document.write("<th>value==0</th>");
document.write("<th>value==null</th>");
document.write("<th>value==\"\"</th>");
document.write("<th>isNaN(value)</th>");
document.write("</tr>");
for (var key in list)
{
var value = list[key];
var r = false;
if (value) {
r = true;
}
document.write("<tr>");
document.write("<td>" + key + "</td>");
document.write("<td>" + (typeof (value)) + "</td>");
document.write("<td>" + value + "</td>");
document.write("<td>" + r + "</td>");
document.write("<td>" + Boolean(value) + "</td>");
document.write("<td>" + (value == true) + "</td>");
document.write("<td>" + (value == false) + "</td>");
document.write("<td>" + (value == 1) + "</td>");
document.write("<td>" + (value == 0) + "</td>");
document.write("<td>" + (value == null) + "</td>");
document.write("<td>" + (value == "") + "</td>");
document.write("<td>" + (isNaN(value)) + "</td>");
document.write("</tr>");
}
document.write("</table>");

2010/03/21

HTML5も

このエントリーをはてなブックマークに追加
HTML5についての話は聞くけれど、実用機能のある一般サイトで「え? これってFlashじゃなかったの?」って後で驚くようなことは、残念ながら今のところ未経験です。

「こんだけすげーことができるんだぜ」というのも「Flashを使わずに」という一文が付いて回っている限り、全然すごくないんだよね。なので、いまいち盛り上がりに欠けるところです。
まあ、ブラウザの能力が急速に上がっていることは、何より楽しい未来を期待させる要因です。

Chrome Experiments
http://www.chromeexperiments.com/

2010/03/20

i++と++iの違い

このエントリーをはてなブックマークに追加

インクリメント時の処理の違い

インクリメントするときに、記述の違いでの処理の違いについて。
i++は値を返してからiに1を加えます。++iは、1を加えてから値を返します。

例としてソースコードはjavascriptのものですが、C#でも同じです。

i++
var i = 0;
var a = i++;

alert("i=" + i + "/a=" + a);
/* 出力結果 i=1/a=0 */

++i
var i = 0;
var a = ++i;

alert("i=" + i + "/a=" + a);
/* 出力結果 i=1/a=1 */

インクリメントのときに、一緒にどこかに代入するようなケース自体が稀です。
whileの条件判定するときにi++か++iを使うのだろうなあと想像はできますけれど、もはやそういうコードを書く機会はあまりないかもしれませんね。けれど、知ってるに越したことはないです。
雑学的なネタ話になってしまってる感もあります。どっちがどっちかごっちゃになって調べる羽目になったので自分メモ。
世の中にはi++派が多いらしい(自分もその一人)。右利きが多いみたいなもんでしょうか。i++しか見たことない気もします。

2010/03/11

[javascript]canvasで円や角丸の矩形を描画する

このエントリーをはてなブックマークに追加

円や角丸の矩形の描画

canvasで円や、角丸の矩形を描く。
関数が用意されていないので自作します。
ソースコード
function draw(){
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    var pi = Math.PI;
    // 塗りつぶし円を描く
    var fillCircle = function (x, y, r) {
        ctx.beginPath();
        ctx.arc(x, y, r, 0, pi * 2.0, true);
        ctx.fill();
    };

    // 塗りつぶし角丸の矩形を描く
    var fillRoundRect = function (l, t, w, h, r) {
        ctx.beginPath();
        ctx.arc(l + r, t + r, r, - pi, - 0.5 * pi, false);
        ctx.arc(l + w - r, t + r, r, - 0.5 * pi, 0, false);
        ctx.arc(l + w - r, t + h - r, r, 0, 0.5 * pi, false);
        ctx.arc(l + r, t + h - r, r, 0.5 * pi, pi, false);
        ctx.closePath();
        ctx.fill();
    }

    var colorList = ["#0080be", "#00b09b", "#a6ff2d", "#ffbe23", "#f70013", "#8200b2"];
    var count = colorList.length;
    for (var i = 0; i < count; i++) {
        ctx.fillStyle = colorList[i];
        var v = i * 40  / count;
        fillCircle(50, 50, 40 - v);
        fillRoundRect(100 + v, 10 + v, 120 - v * 2, 80 - v * 2, (40 - v)  / 2);
    }
}
実行結果
サンプルコードの実行結果はこのとおり。

fill()をstroke()にすれば、塗りつぶしではなく線を描くことができます。

2009/11/10

javascript innerHTML/innerTextを高速化したい

このエントリーをはてなブックマークに追加
documentに対して何らかの操作をすると、それがすぐ画面に反映されるという利点があってとても楽しいんですが、しかし反面アクセスが多くなるとすぐにボトルネックになります。

innerHTMLはめちゃくちゃ汎用的なのですが、文字をちょこっと表示/変更したいだけのようなときには仕様が大げさすぎることもあって、そういうときはinnerTextを使います。
しかしinnerTextよりもTextNodeを直接いじるほうが高速なのではと思って、計測してみた。

●コード

// ①innerHTMLを使う
var func1 = (function(){
var element = document.getElementById("id1");
return function(str){
element.innerHTML = str;
};
})();

// ②innerTextを使う
var func2 = (function(){
var element = document.getElementById("id2");
return function(str){
element.innerText = str;
};
})();

// ③TextNodeのnodeValueを更新する
var func3 = (function(){
var element = document.getElementById("id3");
var textNode = document.createTextNode(count);
element.appendChild(textNode);
return function(str){
textNode.nodeValue = str;
};
})();

●測定
測定方法は、ブラウザごとに上記の関数をそれぞれ、一秒間に何回呼べるかを計測した。

・IE8
① 9388回
② 32648回
③ 59668回

・firefox3
① 18431回
③ 74487回

※innerTextはfirefoxでは使用できないので飛ばします。

・GoogleChrome
① 222675回
② 608540回
③ 642903回

ただし、文字数や処理内容に影響すると思う。どんなときも一秒間にこんなに呼べるとは思えません。
今回はコンテンツ自体もすかすかで、カウンタとなる数値を代入していっただけなので早いのでしょう。

●まとめ
TextNodeのnodeValueを更新したほうが高速。

お決まりの文句ですが、大体の場合、高速化についてはアルゴリズムを見直すのがよいです。
でももっと言うと、クライアントの環境によって処理内容や速度が変わってしまったりするようなものに、速度を求めるのが間違っている気がします。でもこれはほとんど愚痴。

2009/08/24

javascriptの数値計算を高速化

このエントリーをはてなブックマークに追加
javascriptだけじゃなくて、他の言語でも同じようなことです。
なお、javascriptの場合は、クライアントのスペック、および使用されるブラウザに依存する部分が強いようなので、必ずしも全てに当てはまらない可能性があるので注意してください。

なお、よく参照する関数は、var cos=Math.cosというようにあらかじめグローバル変数に代入しておくほうが、参照が減る分高速です。

●商
x/2よりx>>1のほうが高速。ただし、xは整数。同じように、乗算も、x*2よりもx<<1のほうが高速です。>Firefox3
x / 2 = 71
x * 0.5 = 70
x >> 1 = 64

->IE8
x / 2 = 156
x * 0.5 = 141
x >> 1 = 78

->GoogleChrome2
x / 2 = 6
x * 0.5 = 19
x >> 1 = 3

●べき乗
単純に平方を求めるなら、Math.powを使うより同じ数を二回掛けたほうが高速。
Math.pow(x,2)よりx*x。

1000000回テスト(単位はms)
※powはループ前に、var pow=Math.powとして計算したものです。

->Firefox3
Math.pow(x,2) = 321
pow(x,2) = 215
x * x = 95

->IE8
Math.pow(x , 2) = 531
pow(x , 2) = 328
x * x = 94

->GoogleChrome2
Math.pow(x,2) = 143
pow(x,2) = 136
x * x = 4

●小数点以下の切り捨て(いわゆる整数化)
Math.floor(x)よりも、x|0のほうが早い。

1000000回テスト(単位はms)
※ floorはループ前に、var floor =Math. floorとして計算したものです。

->Firefox3.0
Math.floor(x) = 249
floor(x) = 108
x | 0 = 128

->IE8
Math.floor(x) = 468
floor(x) = 219
x | 0 = 94

-> GoogleChrome2
Math.floor(x) = 64
floor(x) = 67
x | 0 = 17

●距離判定
点p1(x1,y1)と点p2(x2,y2)の距離dが、範囲rよりも小さいかどうかの判定だけなら、Math.sqrtを使わなくてもできる。

d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); // 距離の平方
r2 = r * r; // 範囲の平方
if(d2 < r2){
// 近い
}