JavaScript」タグアーカイブ

Mac Application System Event

mac にてスクリプトのみで GUI がやりたい。
つまり Linux の PyGObject のようにしたい。

AppleScript: Graphic User Interface (GUI) Scripting

やっとヒントが見つかった。
System Event で Application を作れば空のアプリになるようだ。
コレを利用して別途で GUI を作ればなんとかなりそう。

シバンで osascript を指定して実行パーミッションを付けて。
拡張子を取っ払わないとデフォルトアプリ起動になるので注意。

#!/usr/bin/osascript -l JavaScript

var app = Application("System Events");
app.includeStandardAdditions = true;
app.displayDialog("はろーわーるど");

javascript_dlg

よし、ダイアログのみを出すスクリプトはイケた!
でも Finder にて command+O では端末が開く…

Python でも試したけど拡張子が無ければシバンで開いてくれる。
けれど Windows と同様に端末が開いてしまう。
pythonw がある時点で気がつけよ俺、Linux には無いてか不要。

端末を開かずに osascript を実行する方法、あるのか?
Windows で PowerShell をブン投げた時を再現するとは。

Mac は想像していたほど Linux 的には使えないかも。

Mac JavaScript

mac ではスクリプトは何を使おう?

筆者が主に Python を使っているのは Linux だったから。
書くまでもなく PyGObject という究極便利なものがあったから。
Linux なんてアップデートマネージャや dnf が Python 製。

使う言語はフレームワークやライブラリで決まる。
簡易な CMS ツールなら wordpress ベースなので PHP に。
スマホゲームなら Unity 一択に近いので C# に。

Mac で Python はどう考えたって役立たずだよなぁ。
Apple スクリプトを覚えても他で応用が効かないのが痛い。
ならば JavaScript なんてどうだ?

なんたって上記のドレも JavaScript が絡む。
今や GNOME デフォルトアプリでさえ Gjs 製がある。
Unity(mono) は JavaScript も使える、というように。
mac でも Gjs のようなものがあれば便利だけど。

知らないうちにMacがシステム標準でJavaScriptで操作できるようになってた (JXA) – Qiita

って思っていたら Yosemite なら普通に使えるんかい!

早速 .bashrc にエイリアスを用意しよう、mjs でいいかな。
どうでもいいけど mac には標準で nano が入っている。
ドットファイルの編集をする場合なんかに便利だね。

nano

PS1='[\u@\h \W]$ '

mjs()
{
        if [ $# -eq 0 ]; then
                osascript -l JavaScript -i
        else
                osascript -l JavaScript $@
        fi
}

普通にエイリアスを登録でもいいけど筆者は関数にした。
これなら Gjs や Python 同様に引数の有無でインタラクティブシェルに。
同じようにしたい人は関数名をお好みで変更してね。

再ログインで使えるようになったのを確認してと。
control+D で終了できるけど終了時に改行はしてくれないようだ。
さて、Gjs 並には使えるのかな?

mac_js

let は無理か、まあ Safari でも使えないからね。
JavaScript エンジンは Safari と同じ Nitro だろうし。
でも Gjs のようにタプルは問題なく使えるようです。
SpiderMonky に統一ってわけにはいかないよな。

標準出力は console.log() を使えば可能。
print() を用意してほしいな、手段を知らないだけかもだが。

さて言語は使えることが解ったので GUI を作る方法でも。

OS X YosemiteからJSでMacアプリを作れるようになったって!?と聞いてみたものの | mah365

こんなのを見つけたけど osascript では動かないや。
スクリプトエディタ.app で command+R の必要がある。
うーん、これじゃたいして役に立たないぞ。

何か手段があるのだろうけど検索しても出てこない。
そりゃこんな Mac らしくないことがしたい人は少ないよNE。
もう少し調べる。

localStorage

二年近く前のネタだけどこんなのを見つけた。

世界が熱狂する「100万のタマゴ」アプリ 日本の個人開発者のアプリが620万DLを達成 | 【EXドロイド(エックスドロイド)】

これならスマートフォン向け Web アプリで作れそう。
しかしパソコン対応なんかにしたらネクラでキモチワルイ奴がスクリプトでアッサリ百万突破してドヤッ!てやるのが目に見える…
まあそれは別の話として。

ゲーム再開用に行うタップ回数の保持は当然ローカルストレージとなるわけで。
スマートフォンのブラウザなら必ず実装しているので使わない手はない。
問題はそのストレージ内と変数値の同期をいつどう行うか。

ページを閉じた時を検知して変数の整数値を保存が一番確実なのだが
window.onbeforeunload – Web API インターフェイス | MDN
これ iPhone で使えなかった。

サイトの後処理にはonpagehideを使う – 読み書きプログラミング ブログ

onpagehide じゃ環境依存になってしまうようだ。
やはり値変更毎に localStorage へ保存するしか確実な手段は無さそう。
しかし[ピアノ打ち]とかされた場合にストレージへの同期は追い付くのかな?

iOSのlocalStorageは非同期でディスクに保存されるっぽい – Qiita

なんてページを発見。
自動的に非同期になるなら何も問題無さそう、コードを書いて実験だ。
毎度のようにスマートフォンかエミュレートでしか動きません。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>タップした数を記憶する</title>
<!-- for Smart Phone -->
<meta name="viewport" content="
    width=device-width,
    initial-scale=1.0,
    minimum-scale=1.0,
    maximum-scale=1.0,
    user-scalable=no" />
<style>
body {
    -webkit-text-size-adjust: 100%;
}
</style>
<script type="text/javascript"><!--

    var count = 0;

    var writeText = function() {        
        var num = 1000000 - count;
        var out = String(num).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,' );
        document.getElementById("ID_TEXT").innerHTML = out;
    }
    // Event Handler
    var onTouchEnd = function() {
        if (count < 1000000) {
            count += 1;
            localStorage.setItem("count", count);
            writeText();
        }
    }
    // Connect
    var init = function() {
        if (window.TouchEvent) {
            var tap = document.getElementById("ID_TAP");
            tap.addEventListener("touchend",onTouchEnd);
        }
        /* can not iPhone
        window.onbeforeunload = function(e) {
            localStorage.setItem("count", count);
            return null;
        );*/
        if (localStorage.getItem("count") != null) {
            count = Number(localStorage.getItem("count"));
        };
        writeText();
    }
    //-->
</script>

</head>
<body onLoad="init()">

<div id="ID_TEXT" style="font-size:48pt">1,000,000</div>
<div id="ID_TAP" style="font-size:96pt">Tap!</div>
<p><a href=".">Back</a></p>
</body>
</html>

タップした数を記憶する

IMG_0097

これだけだと本当につまらないアプリです。
卵がちょっぴりづつ割れていくみたいな演出があると先が気になるってことか。
メモしとこ。

document.createElement

さて、カエル王子を引っ張ってピヨ吉を退治する JavaScript だ。
こんな感じにしてみた。

※canvas サイズは 320×400 に固定
※ピヨ吉構造体を用意、HP や X,Y 座標メンバを保持
※ソレを画面に配置する数だけ配列にしておく
※タイマーでカエルが動く毎に for 文で全数チェック
※ピヨ吉にヒットしていれば HP を減らし 0 なら非表示にして通過する
※ピヨ吉すべての HP がゼロになればクリア

とりあえず二回ヒットするとピヨ吉が消える暫定コードに。
カエル王子は 32x32px にしてみたけど iPhone では少し引っ張り辛いかも。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ひよこを倒す 01</title>
<!-- for Smart Phone -->
<meta name="viewport" content="
    width=device-width,
    initial-scale=1.0,
    minimum-scale=1.0,
    maximum-scale=1.0,
    user-scalable=no" />
<style>
body {
    margin: 0;
    overflow: hidden;
    -webkit-text-size-adjust: 100%;
}
#ID_CANVAS {
    background-color: #FF7700;
}
</style>
<script type="text/javascript"><!--

    var canvas = null;
    var context = null;
    var ball = null;
    var piyos = [];
    // Constant
    const INIT_X = 160;  // X origin of the image
    const INIT_Y = 280;  // Y origin of the image
    const OFFSET = 16;   // Half of the image size
    const INTERVAL = 20; // n/1000 ms
    const LENGTH = 20;   // Distance the image moves
    const COUNT  = 250;  // INTERVAL*n ms
    // struct
    var piyokichi = function(x, y, hp) {
        this.x = x;
        this.y = y;
        this.hp = hp;
        this.img = null;
    }
    var ballData = function() {
        this.timer_id = -1;
        this.timer_count = 0;
        this.x = 0;
        this.y = 0;
        this.rotation = 0;
    }
    var BallData = new ballData();

    /*
     * Touch Event Handler
    **/
    var onBallTouchStart = function(e) {
        e.preventDefault();
    }
    var onBallTouchMove = function(e) {
        if (BallData.timer_id == -1) {
            context.clearRect(0, 0, canvas.width, canvas.height);
            BallData.x = e.touches[0].pageX;
            BallData.y = e.touches[0].pageY;
            if (BallData.y > 400 - OFFSET)
                BallData.y = 400 - OFFSET;
            context.beginPath();
            context.moveTo(INIT_X, INIT_Y);
            context.lineTo(BallData.x, BallData.y);
            context.stroke();
            moveBall(BallData.x, BallData.y);
        }
    }
    var onBallTouchEnd = function(e) {
        if (BallData.timer_id == -1) {
            BallData.rotation = Math.atan2(BallData.y - INIT_Y, BallData.x - INIT_X)
            BallData.timer_count = COUNT;
            BallData.timer_id = setInterval(onTimer, INTERVAL);
            context.clearRect(0, 0, canvas.width, canvas.height);
        }
    }
    /*
     * Timer Handler
    **/
    var onTimer = function() {
        // Clear ?
        var clear = piyos.length;
        for (var i=0; i<piyos.length; i++) {
            if (piyos[i].hp == 0) clear -= 1;
        }
        if (clear == 0) {
            context.font = "64pt 'Arial'";
            context.fillText("Great!", 10, 100);
            clearInterval(BallData.timer_id);
            BallData.timer_id = -1;
            return;
        }
        if (BallData.timer_count > 0) {
            // Hit the Rival ?
            if (!getRivalHit()) {
                // Hit the Wall ?
                if (BallData.x < OFFSET || BallData.x > canvas.width - OFFSET)
                    BallData.rotation = Math.PI - BallData.rotation;
                if (BallData.y < OFFSET || BallData.y > canvas.height - OFFSET)
                    BallData.rotation = -BallData.rotation;
            }
            // Move Image
            BallData.x -= Math.cos(BallData.rotation) * LENGTH;
            BallData.y -= Math.sin(BallData.rotation) * LENGTH;
            moveBall(BallData.x, BallData.y);
            BallData.timer_count--;
        } else {
            clearInterval(BallData.timer_id);
            BallData.timer_id = -1;
            moveBall(INIT_X, INIT_Y);
        }
    }
    /*
     * Hit Function
    **/
    var getRivalHit = function() {
        for (var i=0; i<piyos.length; i++) {
            if (BallData.x > piyos[i].x - OFFSET &&
                BallData.x < piyos[i].x + OFFSET &&
                BallData.y > piyos[i].y - OFFSET &&
                BallData.y < piyos[i].y + OFFSET) {
                // HP == 0?
                piyos[i].hp -= 5;
                if (piyos[i].hp <= 0) {
                    piyos[i].hp = 0;
                    piyos[i].img.style.display = "none";
                    continue;
                }
                if (BallData.x > piyos[i].x - OFFSET && BallData.x < piyos[i].x + OFFSET)
                    BallData.rotation = Math.PI - BallData.rotation;
                if (BallData.y > piyos[i].y - OFFSET && BallData.y < piyos[i].y + OFFSET)
                    BallData.rotation = -BallData.rotation;
                return true;
            }
        }
        return false;
    }
    /*
     * Move Function
    **/
    var moveBall = function(x, y) {
        ball.style.left = x - OFFSET + "px";
        ball.style.top  = y - OFFSET + "px";
    }
    /*
     * Initialize
    **/
    var init = function() {
        if (window.TouchEvent) {
            canvas = document.getElementById("ID_CANVAS");
            canvas.width = window.innerWidth;
            canvas.height = 400;
            context = canvas.getContext("2d");
            ball = document.getElementById("ID_BALL");
            ball.addEventListener("touchstart",onBallTouchStart);
            ball.addEventListener("touchmove",onBallTouchMove);
            ball.addEventListener("touchend",onBallTouchEnd);
            moveBall(INIT_X, INIT_Y);
            // Back Button
            var back = document.getElementById("ID_BACK");
            back.style.top = canvas.height + "px";
            back.addEventListener("touchend", function() {
                document.location = ".";
            });
            // Create Piyokichi
            piyos.push(new piyokichi(20, 200, 10));
            piyos.push(new piyokichi(50, 30, 10));
            piyos.push(new piyokichi(60, 100, 10));
            piyos.push(new piyokichi(170, 80, 10));
            piyos.push(new piyokichi(280, 120, 10));
            for (var i=0; i<piyos.length; i++) {
                var img = document.createElement("img");
                img.alt = "image";
                img.src = "kaeru01/hiyo02_32x32.gif";
                img.width = 32;
                img.height = 32;
                img.style.position = "absolute";
                img.style.left = piyos[i].x - OFFSET + "px";
                img.style.top  = piyos[i].y - OFFSET + "px";
                document.body.appendChild(img);
                piyos[i].img = img;
            }
        }
    }
    //-->
</script>

</head>
<body onLoad="init()">

<canvas id="ID_CANVAS"></canvas>
<img id="ID_BALL" src="kaeru01/kaeru02_32x32.gif" style="position:absolute" alt="ball">
<img id="ID_BACK" src="back.png" style="position:absolute" alt="back">

</body>
</html>

kaeru01

ひよこを倒す 01

敵の攻撃やターン数、障害物その他についてはまた今度。
長くなってきたので次回から[リンク先で Ctrl+U してね!]にするつもり。

他に今回やってみたこと。

canvas に文字列を表示するには fillText
[MS Pゴシック]なんてこのスマートフォン時代に指定しないで下さい。

context.font = "64pt 'Arial'";
context.fillText("Great!", 10, 100);

img タグを動的に作るには createElement
その img を非表示にするには img.style.display = “none”;

var img = document.createElement("img");
document.body.appendChild(img);

後は前回までのコードを少し応用しただけでココまで作れた。
しかしヒット判定は四方チェックしてから振り分けしか手段が無いのかな?
ネットに転がっているブロック崩しのコードを色々見るもイマイチ参考にならないし。
全部グローバル変数で読み辛い人多過ぎだし、構造体やクラスって凄く便利だよ。

今回は[フリー素材 動物アイコン]で適当に検索して以下をお借りしました。
1キロバイトの素材屋さん-無料素材の配布/可愛いアイコン・動く顔文字などのフリー素材集-

カエルとひよこがあって丁度よかった、32x32px に変更しただけ。
本格的に作る時があったらオリジナルキャラにしますが筆者は絵心が…

start, stop, clear

打ち出したボールが何かに衝突して跳ね返るみたいなコードを探す。
究極かもしれないと思えるサンプルコードを発見!

endo blog: HTML5 – Canvas 円同士の衝突アニメーション

しかし例のごとく setInterval で clearInterval していない。
無限に増殖する円が延々衝突し続けるという究極の CPU 殺しでもあった。
一生懸命自己解析していたら CPU ファンが凄い勢いで回り初めて焦った。
もしファンの無いスマホなんかで見ていたら…

しかたがないので停止や初期化のボタンを付けてコピペ。
TimerClass をインスタンス化したら start(func), stop() メソッドで簡単に使えるようにしてみた。
それをスマートフォン用にレイアウト、のつもり。
肝心のアルゴリズムはコピペです、すんません。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>球の衝突(負荷強)</title>
<!-- for Smart Phone -->
<meta name="viewport" content="
    width=device-width,
    initial-scale=1.0,
    minimum-scale=1.0,
    maximum-scale=1.0,
    user-scalable=no" />
<style>
body {
    margin: 0;
    overflow: hidden;
    -webkit-text-size-adjust: 100%;
}
img {
    margin: 0;
    padding: 0;
    overflow: hidden;
}
#ID_CANVAS {
    background-color: #FFFFFF;
}
</style>
<script type="text/javascript"><!--

    var canvas = null;
    var context = null;
    var timer = null;
    var cw = 0;
    var ch = 0;

    var g = 0.1;
    const INTERVAL = 10;
    var circles = [];
    var colorList = ['0','1','2','3','4','5','6','7',
                     '8','9','a','b','c','d','e','f'];

    var TimerClass = function() {
        this.count = 0;
        this.id = -1;
        this.start = function(func) {
            if (this.id == -1) {
                this.id = setInterval(func, INTERVAL);
            }
        }
        this.stop = function() {
            if (this.id > -1) {
                clearInterval(this.id);
                this.id = -1;
            }
        }
    }

    var onTimer = function() {
        context.clearRect(0, 0, cw, ch);
        if (timer.count>100 || timer.count==0) {
            timer.count = 0;
            circles.push(new Circle());
            circles[circles.length-1].init(circles.length % 2);
        }
        for (var i=0; i<circles.length; i++) {
            for(var j=i+1; j<circles.length; j++) {
                collisionCircleCircle(circles[i], circles[j]);
            }
        }
        for (var i=0; i<circles.length; i++) {
            circles[i].move();
            circles[i].collisionWall();
            circles[i].view();
        }
        timer.count++;
    }

    function Circle(){
        this.h = 0.9;
        this.x = 0;
        this.y = 100;
        this.vx = 0;
        this.vy = 0;
        this.r = 0;
        this.color = '#';

        this.init = function(vec) {
            this.vx = Math.random() * 9 + 1;
            this.r = Math.random() * 25 + 5;
            if (vec) {
                this.x = cw + 100;
                this.vx *= -1;
            } else {
                this.x = -100;
            }
            this.color += colorList[Math.floor(Math.random() * 3) + 3];
            this.color += colorList[Math.floor(Math.random() * 5) + 5];
            this.color += colorList[Math.floor(Math.random() * 7) + 9];
        }
        this.move = function() {
            this.x += this.vx;
            this.y += this.vy;
            this.vy += g;
        }
     
        this.view = function() {
            context.beginPath();
            context.fillStyle = this.color;
            context.arc(this.x, this.y, this.r, 0, 360, false);
            context.fill();
        }
     
        this.collisionWall = function() {
            if (this.x+this.r > cw && this.vx > 0 || this.x-this.r < 0 && this.vx < 0) {
                this.vx *= -this.h;
            } else if (this.y+this.r > ch && this.vy > 0 || this.y-this.r < 0 && this.vy < 0) {
                this.vy *= -this.h;
            }
        }
    }

    function collisionCircleCircle(a,b){
        if( (b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y) < (a.r+b.r)*(a.r+b.r) ){
            var vx = a.x-b.x;
            var vy = a.y-b.y;
            var len = Math.sqrt(vx*vx+vy*vy);
            var d = a.r+b.r-len;
            if (len>0)
                len =1/len;

            vx *= len;
            vy *= len;

            d /= 2.0;
            a.x += vx*d;
            a.y += vy*d;
            b.x -= vx*d;
            b.y -= vy*d;

            var t;
            t = -(vx*a.vx+vy*a.vy)/(vx*vx+vy*vy);
            var arx = a.vx+vx*t;
            var ary = a.vy+vy*t;

            t = -(-vy*a.vx+vx*a.vy)/(vy*vy+vx*vx);
            var amx = a.vx-vy*t;
            var amy = a.vy+vx*t;

            t = -(vx*b.vx+vy*b.vy)/(vx*vx+vy*vy);
            var brx = b.vx+vx*t;
            var bry = b.vy+vy*t;

            t = -(-vy*b.vx+vx*b.vy)/(vy*vy+vx*vx);
            var bmx = b.vx-vy*t;
            var bmy = b.vy+vx*t;

            var e = 0.8;
            var adx = (a.r*amx+b.r*bmx+bmx*e*b.r-amx*e*b.r)/(a.r+b.r);
            var bdx = -e*(bmx-amx)+adx;
            var ady = (a.r*amy+b.r*bmy+bmy*e*b.r-amy*e*b.r)/(a.r+b.r);
            var bdy = -e*(bmy-amy)+ady;

            a.vx = adx+arx;
            a.vy = ady+ary;
            b.vx = bdx+brx;
            b.vy = bdy+bry;
        }
    }
    var init = function() {
        if (window.TouchEvent) {
            cw = window.innerWidth;
            ch = window.innerHeight - 60;
            timer = new TimerClass();
            // Create Canvas and Context
            canvas = document.getElementById("ID_CANVAS");
            canvas.width = cw;
            canvas.height = ch;
            context = canvas.getContext("2d");
            // Create ToolBar
            var back = document.getElementById("ID_BACK");
            back.style.top = ch + "px";
            back.style.left = 0 + "px";
            back.addEventListener("touchend", function() {
                document.location = ".";
            });
            var start = document.getElementById("ID_START");
            start.style.top = ch + "px";
            start.style.left = 80 + "px";
            start.addEventListener("touchend", function() {
                timer.start(onTimer);
            });
            var stop = document.getElementById("ID_STOP");
            stop.style.top = ch + "px";
            stop.style.left = 160 + "px";
            stop.addEventListener("touchend", function() {
                timer.stop();
            });
            var clear = document.getElementById("ID_CLEAR");
            clear.style.top = ch + "px";
            clear.style.left = 240 + "px";
            clear.addEventListener("touchend", function() {
                timer.stop();
                circles = [];
                context.clearRect(0, 0, cw, ch);
            });
        }
    }
    //-->
</script>

</head>
<body onLoad="init()">

<canvas id="ID_CANVAS"></canvas>
<img id="ID_BACK" src="button1/back.png" style="position:absolute">
<img id="ID_START" src="button1/start.png" style="position:absolute">
<img id="ID_STOP" src="button1/stop.png" style="position:absolute">
<img id="ID_CLEAR" src="button1/clear.png" style="position:absolute">

</body>
</html>

球の衝突(負荷強)

clash

はっきりいって何がどうなってこうなるかよくワカンネェ!
重力で落下するみたいな処理も多分入っているよな、どこだか解らないけど。
何日かチマチマ弄っていればそのうち少しは理解できる、といいな。
今回は覚書とタイマーの簡単な利用方法、ということで。

それにしても神様、私にもっと絵心というものを下さい。
ボタンが我ながらダサすぎる、今後の課題はイラストかも。