更新日: 2024年12月27日


V. JavaアプレットをJavaScriptで代替(3)

4. 2024年に作成したJSプログラム

駄待ち狐はともちゃんHPを開設した1996年からHTMLを書いており、1997年にはJSを使っていました。CSSを使い始めたのがいつかは定かではありませんが、少なくとも2015年にJavaアプレットをJSに移植した時にはCSSを使っていました。ただ、今やHP作成の3種の神器となっているHTML、CSS、JSについて、最新のトレンドを学習していないなあという反省がありました。そこで2024年に2023年初版の教科書を買って、改めて勉強し直しました。CSSについては大いに参考になったのですが、JSについては「最新の」教科書ではありませんでした。

実はこの教科書、古い版に加筆修正したもので、JSについては古いままの部分と新しく追加した部分の2本立てになっていたのです。それほどJSの変化が激しいということだと思いますが、教科書の構成としては如何なものかという気がしました。何にしても、JSの新しいトレンドがスマホ対応とイベントリスナーだということは分りました。スマホ対応についても本節の最後で述べますが、イベントリスナーは「I. 人生の余技として作成したプログラム」の「6. JavaScript」で書いた「コードはコードらしくしなさい」と「お説教」された結果です。

そこで書いた通り、HTML側の<INPUT>要素の中で<SCRIPT LANGUAGE="JavaScript">とも書かずに"onClick="を使ってJSの関数を呼出すというのは、コードの建付けとして違和感があります。「3. 2015年に作成したJSプログラム」で紹介したpushBttn.htmlでも、<DIV>要素の中で"onMouseDown="を使ってJSの関数を呼出しているので同じことです。これではダメだと言うので登場したのがイベントリスナーで、JS側でHTML要素にイベントが発生したことを検知します。この後紹介するJSは全てイベントリスナーを使っており、最後に紹介するのはイベントリスナーを使ってスマホの回転対応をするJSです。

イベントリスナーの基本は

(HTML要素).addEventListener(イベント名, 関数名);

ですが、この関数名のところに直接関数を書くことも可能で、この場合は無名関数

function (引数) { 処理; }

を使います。さらに、無名関数の簡略記法としてアロー関数

(引数) => { 処理; }

を用いることもできます。結局のところ、アロー関数を使ったイベントリスナーは

(HTML要素).addEventListener(イベント名, (引数) => {
    処理;
});

となり、これが最新の書き方です。引数がない場合は"() => {"となるので、いきなりこれを見せられると「古代文明の象形文字??」となります。

4.1 playPark.js

playPark.jsはPlayer.classのJS版で、今の「大阪の公園めぐり」で使っているものです。一旦は削除した「大阪の公園めぐり」ですが、JSの最新トレンドを勉強したのを機に復活しようと思い立ちました。playPark.htmlではBGMの<audio>タグとアニメーションの<img>タグが並んでいるだけです。これを連動させるのはplayPark.jsに仕込んだイベントリスナーの役割です。連動のさせ方は色々あると思いますが、playPark.jsでは<audio>タグ内のcontrolsによって表示されるコントロールバーのプレイボタンが押されると、アニメーションを開始するようにしました。これが22~25行目のイベントリスナーです。

コードの表示にはPrism.jsを使用しています。
prism.cssの設定は団塊爺ちゃんの備忘録を参考にしました。

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="playPark.js"></script>
</head>
<body bgcolor="#3fb6bf">
<center>
<p>
<audio id="sound" src="https://tomochan.jp/images/S1.mp3" controls>
</audio>
</p>
<p id="Panim">
<img id="anim" src="https://tomochan.jp/images/T0.gif" width=455 height=390>
</p>
</center>
</body>
</html>

let iFrame;
let iSlide;
let reqId;
let loopCnt = 0;
const imgNum = 30;
const idleFr = 28;

function renderLoop() {
    if(iFrame % idleFr == 0) {
        iSlide = iFrame / idleFr + 1;
        document.getElementById('anim').src = "https://tomochan.jp/images/T" + iSlide + ".gif";
    }
    reqId = window.requestAnimationFrame(renderLoop);
    if(iSlide < imgNum) {
        iFrame++;
    } else {
        window.cancelAnimationFrame(reqId);
    }
}

window.addEventListener('load', () => {
    document.getElementById('sound').addEventListener('play', () => {
        iFrame = 0;
        renderLoop();
    });
    document.getElementById('sound').addEventListener('ended', () => {
        if(loopCnt < 1) {
            document.getElementById('sound').play();
            loopCnt++;
        } else {
            location.href = 'https://tomochan.jp/guides/map.html';
        }
    });
});

24行目のコマンドで開始されたrenderLoop()関数は、17行目のcancelAnimationFrame()によって1回の上映だけで終了します。requestAnimationFrame()は60Hzなのに対してアニメーション画像は毎秒2フレーム程度の枚数なので空送りが必要ですが、空送りの枚数をidleFrという変数にして調整することでBGMの最後とアニメーションの最後が一致するようにしています。一方、26~33行目のイベントリスナーは、BGMが終了した時の処理です。1回目の終了時はloopCntが0なので再度プレイし、その結果としてアニメーションも再上映されます。2回目が終了した時は、map.htmlにジャンプします。

以上の2つのイベントリスナーは、21行目のイベントリスナー「ドキュメントのロードが完了したら実行する」の中に入れ子になっています。これはどういうことかというと、playPark.jsはplayPark.htmlの<head>で呼出されるので、この時点で<body>にある<audio id="sound" ...>は読込まれておらず、document.getElementById('sound')を実行するとエラーになってしまうのです。そのため、ドキュメント全体がロードされるのを待ってから22~33行目を実行する必要があります。getElementById()が登場した当初はHTMLの最後にJSコードを置くという便法もありましたが、これまた「コードらしくない」やり方です。

4.2 pointMap.js

pointMap.jsはPointer.classのJS版で、今のガイドマップで使っているものです。勉強した教科書には出てこなかったのですが、色々グーグっているとHTMLの<canvas>要素をJSのオブジェクトにしてそのgetContext()メソッドを使うと、JSでも図形の描画ができることが分りました。このことを知らなければ、pushBttn.jsの時のように「丸印のgifをたくさん用意して」となるところでした。pointMap.htmlの7行目で必要なサイズのキャンバスを準備して、pointMap.jsの10~11行目でctxオブジェクトを生成すれば、ctxのメソッドを使って描画することができます。引数を'2d'にすることで2次元描画用のオブジェクトが生成されます。


<html>
<head>
<script type="text/javascript" src="pointMap.js"></script>
</head>
<body bgcolor="#ffffb0">
<center style="margin-top: 30;">
<canvas id="canvas" width="446" height="597"></canvas>
</center>
</body>
</html>

let pnum = -1;
const xp = [272,264,376,402,364,331,382,265,276,304,336,326,355,238,170,234,145,96];
const yp = [197,132,209,163,229,315,281,332,332,363,453,425,398,385,466,465,489,537];
const map = new Image();
map.src = 'map.gif';
const help2 = new Image();
help2.src = 'help2.gif';

window.addEventListener('load', () => {
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    ctx.drawImage(map, 0, 0);

    // i? 1 or 2: red, 3 or 4: green, 1 or 3: large, 2 or 4: small, 5: double
    function putCircle(i, x, y) {
        let r;
        if(i < 3) {
            ctx.fillStyle = '#ff0000';
        } else {
            ctx.fillStyle = '#00ff00';
        }
        if(i % 2 == 0) {
            r = 6;
        } else if(i !== 5){
            r = 7.5;
        } else {
            r = 9.5;
        }
        if(i === 5) {
            ctx.strokeStyle = '#ff0000';
            ctx.lineWidth = 4;
        }
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();
        if(i === 5) {
            ctx.stroke();
        }
    }

    canvas.addEventListener('mousedown', (e) => {
        const x = e.offsetX;
        const y = e.offsetY;
        if((x >= 10) && (x <= 200) && (y >= 61) && (y < 385)) {
            if(pnum !== -1) {
                putCircle(3, xp[pnum], yp[pnum]);
                putCircle(4, 20, 70 + pnum * 18);
            } else {
                ctx.drawImage(help2, 262, 4);
            }
            pnum = Math.trunc((y - 61) / 18);
            putCircle(1, xp[pnum], yp[pnum]);
            putCircle(2, 20, 70 + pnum * 18);
        }
        if(Math.abs(x - xp[pnum]) < 8 && Math.abs(y - yp[pnum]) < 8) {
            putCircle(5, xp[pnum], yp[pnum]);
        }
    });

    canvas.addEventListener('mouseup', (e) => {
        const x = e.offsetX;
        const y = e.offsetY;
        if(Math.abs(x - xp[pnum]) < 8 && Math.abs(y - yp[pnum]) < 8) {
            location.href = `https://tomochan.jp/guides/park${pnum + 1}_j.html`;
        }
    });
});

キャンバス上に画像をはめ込むのは簡単で、12行目と50行目にあるようにctx.drawImage()で画像オブジェクトとキャンバス上の相対座標を指定すればOKです。ここで読込んでいるGIFファイルはZIPでダウンロードできます。一方、キャンバス上に丸印を描くのは結構面倒で、33~36行目にある4行が必要です。キャンバスではパス(経路)を描くのが基本で、34行目で360度(2π rad)の円弧を描き、36行目でこれを塗りつぶしています。15~40行目のputCircle()関数は引数iが1なら大きい赤丸、2なら小さい赤丸、3なら大きい緑丸、4なら小さい緑丸、5なら緑の丸の外周に赤の円環を描きます。38行目ではパスそのものに幅を付けて円環にしています。

42~59行目のイベントリスナーは、キャンバス要素上でマウスボタンが押されたことを検知します。42行目のアロー関数は引数がイベントオブジェクトeであり、43~44行目でカーソル位置(x, y)を取得できます。この後の動作はPointer.javaと同じですが、pnumが前の公園番号の状態で緑丸を描き、新しいpnumに更新した後に赤丸を描くことで、ppnumは不要にしています。56~58行目は二重丸を描く処理ですが、この時点ではまだ公園のページにジャンプしません。61~67行目でマウスボタンが離されたことを検知するとジャンプしますが、押した赤丸の外にカーソルを移動して離した場合はジャンプしません。

4.3 playSlides.js

playSlides.jsは今の「道路族?公園族!」で使っているもので、showSlides.jsに自作のBGMを追加しました。playPark.jsとほぼ同じですが、「2回目が終了した時」という部分がなく何回でも演奏を繰返します。このBGMは"Pew Summer"という曲名で、ネットが超低速だった時代に数コマで作った動画もどき「ともちゃん大笑い」(483KB)をダウンロードする間(2~3分)流す保留音でした。MIDIをQTムービーにしたmovファイルは4KBしかないなので、すぐに開始できました。因みに「ともちゃんビデオ」のBGMの曲名は"Bay Solome Duas"、「大阪の公園めぐり」は"Lee Po Ping"で、3曲ともMIDIGraphyで作曲したものです。


<html>
<head>
<script type="text/javascript" src="playSlides.js"></script>
</HEAD>
<BODY BGCOLOR="#3fb6bf">
<CENTER>
<audio id="sound" src="http://tomochan.jp/roadmap/S1.mp3" style="width: 400px; height: 30px;" controls></audio>
<p>
<img id="anim" src="http://tomochan.jp/roadmap/T0.gif" width=520 height=520>
</p>
</center>
</body>
</html>

let iFrame;
let iSlide;
let reqId;
const imgNum = 15;
const idleFr = 120;

function renderLoop() {
    if(iFrame % idleFr == 0) {
        iSlide = iFrame / idleFr + 1;
        document.getElementById('anim').src = "http://tomochan.jp/roadmap/T" + iSlide + ".gif";
    }
    reqId = window.requestAnimationFrame(renderLoop);
    if(iSlide < imgNum) {
        iFrame++;
    } else {
        window.cancelAnimationFrame(reqId);
    }
}

window.addEventListener('load', () => {
    document.getElementById('sound').addEventListener('play', () => {
        iFrame = 0;
        renderLoop();
    });
    document.getElementById('sound').addEventListener('ended', () => {
        document.getElementById('sound').play();
    });
});

4.4 phoneRot.js

phoneRot.jsはスマホ対応のJSです。何をするものかを理解していただくために、下のQRコードをスマホで読取って「大阪の公園めぐり」にアクセスしてみて下さい。もし、本サイト自体をスマホで読んでおられる方がいたら、QRコードの長押しでアクセスできます。左側はphoneRot.jsを使っておらず、右側は使っています。一見、何の違いもありませんが、アニメーションの再生中にスマホを横向きにしてもらうと、違いは一目瞭然だと思います。左側は縦のものが横になるだけですが、右側はアニメーションだけになって画面が拡大されます。スマホに期待されるのは、当然のことながら右側の動作です。

右側の動作を実現しているphoneRot.jsのコードは以下の通りです。上記の例ではpark_j.htmlと連動しているので、これも併記します。スマホの向きを検知しているのはphoneRot.jsの18行目と24行目にあるwindow.orientationです。これが±90であればスマホは横向きということになります。17行目のイベントリスナーでは、最初からスマホが横向きの場合にdirLand()関数を実行します。23行目のイベントリスナーではスマホが回転されたことを検知して、横向きになった時はdirLand()関数を実行します。dirLand()のLandはlandscape(横向き)という意味です。因みに縦向きはportraitです。


<!DOCTYPE html>
<html>
<head>
<metaA http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=0.7">
<titleE>
大阪の公園めぐり
</title>
<link rel="stylesheet" type="text/css" href="pushBttn.css">
<link rel="stylesheet" type="text/css" href="audio.css">
<scriptT type="text/javascript" src="pushBttn.js"></script>
<script type="text/javascript" src="guides/playPark.js"></script>
<script type="text/javascript" src="phoneRot.js"></script>
</head>

<body bgcolor="#3fb6bf" link="#0000ff" vlink="#ff4000" alinkK="#ff4000" text="#000000">
<center>

<div id="header">
<H1>
大阪の
<img align=top src="facemark.gif">
公園めぐり
</h1>
<font size=5>
<b>
!!! JavaScriptアニメとガイドマップ !!!
</b>
</font>
<p>
ともちゃんが散歩に出かけるアニメーションの後に大阪府営公園をご案内します。
<br>
Javaスクリプト対応のブラウザでご覧になれます。
</p>
<form type="#" name="hid_prm">
	<input type="hidden" name="offset_x">
	<input type="hidden" name="dname" VALUE="park">
	<input type="hidden" name="dlang" VALUE="_j">
</form>
<div id="wrapMn" style="margin-bottom: 20px;" onMouseDown="jump(2)" onMouseUp="jump(3)">
	<img src="menu_j.gif" id="imageMn" onLoad="jump(1)">
</div>
<hr align=center width=100% size=4>
<p>
<audio id="sound" src="images/S1.mp3" controls>
</audio>
</p>
</div>

<p id="Panim">
<img id="anim" src="images/T0.gif" width=455 height=390>
</p>

<div id="footer">
<div id="wrapGo" onMouseDown="push(0, 'shadeGo2.gif')" onMouseUp="push(1, 'guides/map.html')">
<img src="gomap_j.gif" id="imageBt" >
</div>
</p>
<hr align=center width=100% size=2>
<p>
<div id="wrapMn2" style="margin-bottom: 20px;" onMouseDown="jump(22)" onMouseUp="jump(3)">
	<IMG src="menu_j.gif" id="imageMn2" onLoad="jump(12)">
</div>
</p>
</div>

</center>
</body>
</html>

function dirLand() {
    if(document.getElementById('Lslide') !== null) {
        document.body.style.zoom = 1.25;
    } else if(document.getElementById('imageTVset') !== null) {
            document.body.style.zoom = 1.35;
    } else if(document.getElementById('FMimage') !== null) {
            document.body.style.zoom = 1.15;
    } else if(document.getElementById('Panim') !== null) {
            document.body.style.zoom = 1.15;
    } else {
        document.body.style.zoom = 1.4;
    }
    document.getElementById('header').style.display = 'none';
    document.getElementById('footer').style.display = 'none';
}

window.addEventListener('load', () => {
    if(Math.abs(window.orientation) == 90) {
        dirLand();
    }
});

window.addEventListener('orientationchange', () => {
    if(Math.abs(window.orientation) == 90) {
        dirLand();
    } else {
        document.body.style.zoom = 1.0;
        document.getElementById('header').style.display = 'block';
        document.getElementById('footer').style.display = 'block';
    }
});

dirLand()関数の13~14行目ではpark_j.htmlの中で<div id="header">と<div id="footer">で指定された範囲を非表示にします。アニメーション画面の上側と下側になります。スマホを縦向きに戻した時には28~29行目で表示が復活します。一方、9行目ではアニメーション画面を拡大しています。phoneRot.jsはpark_j.htmlだけでなく「ともちゃんテレビ」と「ともちゃん生放送」でも使われており、さらに写真表示のフレームphoto_j.cgiや大判スライドショーを表示するフレームLslide_j.htmlにも使っています。それぞれ最適の拡大率が異なるので、html/cgiに含まれる要素のidによってどのページかを識別しています。


"() => {"は古代文明の象形文字だとか、キャンバス上に丸印を描くのが面倒だとか文句を書きましたが、JavaScriptは歌って踊れる大スターに育ったなあと感じます。それに対してJavaアプレットは、元々のJavaが重厚長大すぎてwebサイトで使うには「牛刀をもって鶏を割く」感があります。基本構文にエラー処理まで含めることで「私、失敗しないので」と自慢したい気持ちは分かるのですが、やはりwebには「通信とかで失敗したら、再操作したらええやん」という軽さが適しています。脆弱性の問題がなかったとしても、Javaアプレットは早晩JSに取って代られていたような気がします。

一方で「脆弱性」の一言で技術が葬り去られるのは残念なことです。Power Mac G4上で改めて確認しましたが、QTムービーのPewSummer.movは4KB(3,530バイト)しかないのにちゃんと演奏可能です。これをMPEG3に変換したtomochan.jp/roadmap/S1.mp3は876KBです。なぜこんなことになるのかというと、QTムービーはMIDIGraphyで作曲したMIDIデータのままなのに、MPEG3ではオーディオデータに変換されているためです。QTムービーはオーディオデータも扱えますが、MIDIはMIDIのまま処理できるのです。こんなに素晴しい技術が埋れてしまうのは、駄待ち狐が伏せたままでいること以上に勿体ない話だと思います。

JavaアプレットをJavaScriptで代替(1)に戻る