更新日: 2024年12月20日


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

1. ともちゃんHPに施した色々な細工

「I. 人生の余技として作成したプログラム(2)」で述べた通り、ともちゃんHPに動的コンテンツを導入するため、Perl、Javaアプレット、JavaScript(JS)を使い始めました。HPを開設した翌年の1997年には、下記のラインナップを取揃えることができました。新旧のcount.cgiについては「4. Perl」で、新しいindex_j.phpについては「8. PHP」で紹介した通りです。「色変りで流れるヘッドライン」は現在停止していますが、停止前のページはこんな感じでした。本項では「5. Java」」で予告した「サンプルコードを試行錯誤でひねくり回して、何とか所望の動きをするアプレットをビルド」した話と、それをJSに移植した話をします。

	ファイル名			 使用目的					現在の状況
 [Perl]
	count.cgi		 アクセスカウンタ			MySQLを使うものに全面変更
	photo_j.cgi	 写真表示のフレーム			修正して使用中
	form_j.cgi		 お便りの投稿フォーム		廃止
 [Java]
	Runner.class	 色変りで流れるヘッドライン	2015年にJS版を作成(停止中)
	Pusher.class	 ジャンプボタン			2015年にJS版を作成
	Jumper.class	 メニューボタン			   〃
	Projector.class	 ともちゃんテレビの表示		   〃
	Player.class	 公園めぐりのアニメ表示		2024年にJS版を作成
	Pointer.class	 公園めぐりのイメージマップ	   〃
 [JS]
	index_j.html	 冒頭のあいさつ文			index_j.phpにしてPHPを使用
	days_j.html	 何日目ですか?			廃止
	photo_j.html	 日常19までにある写真一覧		そのまま使用中

コードの説明に入る前に、まずアプレット版の動作を見ていただきます。Javaアプレットに対応するブラウザはもうないんじゃないのと思われるでしょうが、Power Mac G4を動態保存してあります。G4のSafariを使い、(このSafariの古い暗号化方式では外部サイトを見られないので)G4にApacheサーバを立てれば、アプレットが見られます。この動画を撮ろうとしたら落し穴がありました。G4はディスプレイを認識しないと映像信号を出力しないため、キャプチャボードでは画面を取込めません。仕方がないので、スマホで画面を撮影するという、ある意味ユーチューバー的なアプローチになりました。その動画が以下のものです。

見ての通り、Runner.class、Player.class、Pointer.class、Pusher.classが今のJS版と同じ動きをします。もちろん、アプレット版と同じ動きになるようにJS版を作ったというのが本当のところです。ただ、今のブラウザではページを開くといきなり音が出るのはNGなので、JS版ではスタートボタンを押さないとアニメの再生を開始しません。オフィス等で急に音がすると周りの人がびっくりするというのが理由のようなので、ボタンを押すとともちゃんが「アハハん」と笑うという機能も、JS版では無くしました。

2. Javaプログラム

Javaはクラスベースのオブジェクト指向言語で、メインプログラム自体もクラスです。クラスはクラスインスタンス(オブジェクト)の雛型で、フィールド(変数、他の言語ではプロパティと呼ばれる)とメソッド(関数)を持っています。スーパー(親)クラスを継承したサブ(子)クラスを定義することができ、子クラスは親クラスのフィールドとメソッドをそのまま引継いだ上で、新たなフィールドやメソッドを追加したり、親クラスと同名のメソッドを定義することでオーバーライド(上書き)したりできます。ここまでのことは、Javaの教科書を読んで駄待ち狐の頭に入っていました。

上記の6つのアプレットは、全てjava.applet.Appletという親クラスを継承する子クラスです。教科書には「親クラスの中身が分らなくても子クラスのプログラムはできる」みたいなことが書いてあったのですが、これは全くの嘘っぱちです。親クラスにどんなメソッドがあって、どんな働きをしているのかが分らなければ、メソッドを上書きしようがありません。本当は「親クラス全体の動作について詳細を把握していなくても、全ての(少なくとも上書きすべき)メソッドの役割と動作が分っていれば子クラスのプログラムはできる」と言うべきです。

今であれば、グーグればjava.applet.Appletに含まれるメソッドの役割と動作が分かるし、ソースコードも見られるでしょう。しかし、1997年当時にはそんな芸当はできません。駄待ち狐はMRJSDK (Macintosh OS runtime for Java software developer kit)に入っているいくつものサンプルコードを穴があくほど眺め、所望の動作に少しでも近付くように細部を修正してはビルドして動作を確認するという作業を繰返しました。真っ暗闇の洞窟の迷路を手探りで進むようでしたが、何とか6つとも狙い通りの動作が実現できました。奇跡的な快挙です。

2.1 Runner.java

Runner.classはトップページのタイトル画像下にヘッドラインを表示するもので、ヘッドライン文字列の先頭は車いすに乗ったともちゃんです。そのソースコードであるRunner.javaを以下に示します。お決りのインポート文、クラスの宣言、変数の型宣言の後、オーバーライドするメソッドが続きます。java.applet.Appletの基本機能はタイミングに応じて指定された場所に指定された画像を表示することで、87行目のpaint(Graphics g)メソッドでこれらを指定します。103行目の"g.drawImage(image, x1, y1, this);"で車いすのともちゃん(image)を表示し、119行目の"g.drawString(label, x2, y2);"で文字列(label)を表示します。

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

import java.awt.*;
import java.applet.*;
import java.lang.Math;
import java.net.URL;
import java.net.MalformedURLException;

public class Runner extends java.applet.Applet implements Runnable
	{
	URL url;
	Image image;
	Font font;
	Color fcolor, bcolor;
	Thread thread;
	MediaTracker tracker;
	String fname, fstyle, label;
	int appWidth, gifWidth, fsize, speed;
	int red, green, blue, xoff, yoff, flame;

	public void init()
		{
		String att = getParameter("gifWidth");
		gifWidth = (att == null) ? 50 : (Integer.valueOf(att).intValue());
		att = getParameter("fname");
		fname = (att == null) ? "TimesRoman" : att;
		att = getParameter("fsize");
		fsize = (att == null) ? 24 : (Integer.valueOf(att).intValue());
		att = getParameter("fstyle");
		fstyle = (att == null) ? "Plain" : att;
		if(fstyle == "Bold")
			{
			font = new Font(fname, Font.BOLD, fsize);
			}
		else
			{
			font = new Font(fname, Font.PLAIN, fsize);
			}
		att = getParameter("label");
		label = (att == null) ? "Blank" : att;
		att = getParameter("speed");
		speed = (att == null) ? 10 : (Integer.valueOf(att).intValue());
		att = getParameter("red");
		red = (att == null) ? 256 : (Integer.valueOf(att).intValue());
		att = getParameter("green");
		green = (att == null) ? 256 : (Integer.valueOf(att).intValue());
		att = getParameter("blue");
		blue = (att == null) ? 256 : (Integer.valueOf(att).intValue());
		bcolor = new Color(red, green, blue);
		att = getParameter("xoff");
		xoff = (att == null) ? 0 : (Integer.valueOf(att).intValue());
		att = getParameter("yoff");
		yoff = (att == null) ? 0 : (Integer.valueOf(att).intValue());
		appWidth = size().width;
		flame = 0;
		}

	public void run()
		{
		try
			{
			url = new URL(getDocumentBase(), "wchair.gif");
			}
		catch(MalformedURLException e)
			{
			}
		image = getImage(url);
		tracker.addImage(image,0);
		try
			{
			tracker.waitForID(0);
			}
		catch(InterruptedException e)
			{
			}
		while (true)
			{
			try
				{
				Thread.currentThread().sleep(1000/speed);
				}
			catch(InterruptedException e)
				{
				}
			repaint();
			}
		}

	public void paint(Graphics g)
		{
		setBackground(bcolor);
		if (tracker.checkID(0))
			{
			int x1 = 0;
			int y1 = 0;
			if(flame == 69)
				{
				flame = 0;
				}
			if(flame <= 55)
				{
				x1 = appWidth-gifWidth - (appWidth-gifWidth)*flame/55;
				}
			flame++;
			g.drawImage(image, x1, y1, this);
			int x2 = x1 + gifWidth + xoff;
			int y2 = font.getSize() + yoff;
			g.setFont(font);
			FontMetrics fm = g.getFontMetrics();
			switch((flame/10) % 7)
				{
				case 0: fcolor = new Color(255, 0, 0); break;
				case 1: fcolor = new Color(255, 144, 0); break;
				case 2: fcolor = new Color(255, 255, 0); break;
				case 3: fcolor = new Color(0, 255, 0); break;
				case 4: fcolor = new Color(0, 0, 255); break;
				case 5: fcolor = new Color(144, 0, 255); break;
				case 6: fcolor = new Color(255, 0, 255); break;
				}
			g.setColor(fcolor);
			g.drawString(label, x2, y2);
			}
		}

	public void start()
		{
		tracker = new MediaTracker(this);
		thread = new Thread(this);
		thread.start();
		}

	public void stop()
		{
		thread.stop();
		}

    public boolean mouseEnter(java.awt.Event evt, int x, int y)
		{
		showStatus("");
		return true;
		}
	}

もう少し詳しく見て行きます。7行目にある通り、RunnerクラスはAppletクラスを継承するだけではなく、アニメーションを表示するためにRunnableというインターフェース(子クラスで利用されるフィールドとメソッドを定義したもの)を実装しています。19行目からのinit()はインスタンスが生成される時に実行されるメソッドですが、アプレットを呼出したHTMLからパラメータを受取っています。index_j.phpの方は以下のようになっています。先程述べたlabelはここで設定しており、HTML上で文字列の変更が可能です。red、green、blueは背景色の設定に使われるRGBで、(63, 182, 191)はともちゃんHP創設以来の背景色3FB6BFです。


<APPLET CODE="Runner.class" WIDTH=460 HEIGHT=60>
<PARAM NAME="gifWidth" VALUE="70">
<PARAM NAME="fname" VALUE="Helvetica">
<PARAM NAME="fsize" VALUE="24">
<PARAM NAME="fstyle" VALUE="Plain">
<PARAM NAME="label" VALUE="書初めはやりたいことを書いたよ。">
<PARAM NAME="speed" VALUE="10">
<PARAM NAME="red" VALUE="63">
<PARAM NAME="green" VALUE="182">
<PARAM NAME="blue" VALUE="191">
<PARAM NAME="xoff" VALUE="5">
<PARAM NAME="yoff" VALUE="15">
</APPLET>

56行目からのrun()はRunnableのメソッドで、Runnableを使って作成したスレッドを開始すると、独立して実行されるスレッド内で呼出されます。このスレッドでヘッドラインが色変りで流れるというアニメーションを制御します。78行目のsleep()でフレームレートを設定し、83行目のrepaint()で87行目のpaint()メソッドが呼出されます。repaint()でpaint()が呼出されるというのも変な話ですが、オーバーライドしていない親クラスのrepaint()メソッドが関与しています。123行目のstart()、130行目のstop()でスレッドを開始・停止し、135行目のmouseEnter()はヘッドライン上にマウスを持って行った時の動作(何もしない)です。

2.2 Pusher.java

Pusher.classはリンクボタンを表示するものです。「そんなものわざわざアプレットにしなくても」と言われそうですが、ともちゃんHPのリンクボタンには駄待ち狐のこだわりが詰っています。ボタンのgif画像自体、立体的に見えるように左と上には明るめの影、右と下には暗めの影が付けてありますが、マウスクリックでボタンを押す(mouseDown)と引っ込んだように影が変ります。この時も立体的に見えるように左と上には暗めの影、右と下には明るめの影を付けます。この引っ込んだ時の影を付けるためにアプレットにしたのです。


import java.awt.*;
import java.applet.*;
import java.lang.Math;
import java.net.URL;
import java.net.MalformedURLException;

public class Pusher extends java.applet.Applet{
	URL button, anchor, urlsnd;
	Image image;
	AudioClip sound;
	Color ltcolor, dkcolor;
	MediaTracker tracker;
	String bname, href;
	int bgcolor, appWidth, appHeight, flag;

	public void init(){
		String att = getParameter("bname");
		bname = (att == null) ? "" : att;
		att = getParameter("href");
		href = (att == null) ? "" : att;
		att = getParameter("bgcolor");
		bgcolor = (att == null) ? 0 : (Integer.valueOf(att).intValue());
		appWidth = size().width;
		appHeight = size().height;
		flag = 0;
		if(bgcolor == 0){
			ltcolor = new Color(76,224,235);
			dkcolor = new Color(46,140,147);
		}else{
			ltcolor = new Color(255,255,255);
			dkcolor = new Color(182,182,126);
		}
		try{
			urlsnd = new URL(getDocumentBase(), "click.au");
		}
		catch(MalformedURLException e){
		}
		sound = getAudioClip(urlsnd);
	}

	public void update(Graphics g){
		paint(g);
	}

	public void paint(Graphics g){
		if(flag == 0){
			try{
				button = new URL(getDocumentBase(), bname+".gif");
			}
			catch(MalformedURLException e){
			}
			image = getImage(button);
			tracker.addImage(image,0);
			try{
				tracker.waitForID(0);
			}
			catch(InterruptedException e){
			}
			if (tracker.checkID(0)){
				g.drawImage(image, 0, 0, this);
			}
		}else{
			g.setColor(dkcolor);
			g.fillRect(0, 0, appWidth-1, 2);
			g.fillRect(appWidth-1, 0, 1, 1);
			g.fillRect(0, 0, 2, appHeight-2);
			g.fillRect(0, appHeight-2, 1, 1);
			g.setColor(ltcolor);
			g.fillRect(1, appHeight-2, appWidth-1, 2);
			g.fillRect(0, appHeight-1, 1, 1);
			g.fillRect(appWidth-2, 2, 2, appHeight-2);
			g.fillRect(appWidth-1, 1, 1, 1);
		}
	}

	public void start(){
		tracker = new MediaTracker(this);
	}

	public boolean mouseEnter(java.awt.Event evt, int x, int y){
		showStatus("http://tomochan.jp/"+href+".html");
		return true;
	}

	public boolean mouseDown(java.awt.Event evt, int x, int y){
		flag++;
		repaint();
		sound.play();
		return true;
	}

	public boolean mouseUp(java.awt.Event evt, int x, int y){
		try{
			anchor = new URL(getDocumentBase(), href+".html");
		}
		catch(MalformedURLException e){
		}
		getAppletContext().showDocument(anchor);
	return true;
	}
}

26~32行目で引っ込んだ時の影の色を指定していますが、ともちゃんHPには背景色が3FB6BFのページ以外にFFFFB0のページがあります。それぞれで影の色を変える必要があるので、都合4色を指定しています。paint(Graphics g)メソッドの中の63~72行目が影を付けるコマンドで、色と座標を指定して矩形を描きます。4つの影を付けるのに8つの矩形を描いているのは、角が斜めになるように1つの影を長短2つの矩形にしているからです。85行目のmouseDown()メソッドでrepaint()します。88行目のsound.play()は「アハハん」です。80行目のmouseEnter()メソッドでは、ブラウザのステータスバーにリンク先のURLを表示します。

2.3 Jumper.java

Jumper.classは各ページの最初と最後(トップページは最後だけ)にあるメニューボタンを表示します。ボタンを立体的に見せるところはPusherをそのまま踏襲しています。mouseDownで「アハハん」というのも同じですが、mouseEnterでは87行目もしくは89行目のコメントを表示します。これに加えての機能は、複数のボタンのどれが押されたかを認識してそのページにジャンプする、現在表示中のページのボタンは最初から押されているように表示する、ここをクリックすると対応する他言語のページにジャンプするという3点です。


import java.awt.*;
import java.applet.*;
import java.lang.Math;
import java.net.URL;
import java.net.MalformedURLException;

public class Jumper extends Applet{
	URL menu, anchor, urlsnd;
	Image image;
	AudioClip sound;
	Color bgcolor, ltcolor, dkcolor;
	MediaTracker tracker;
	String dname, dlang, cgi, extension;
	int i0=99;
	String [] bname = {"index","whois","growth1","growth2","growth3","life1","life6","life11","life16","life21","life26","life31","life36","life41","life42","life43","life44","life45","life46","life47","tomotv","tomofm","park","road","mail"};
	int [] xd = {39,39,39,14,14,39,14,14,14,14,14,14,14,14,9,9,9,9,14,9,39,39,39,39,39};
	int [] x0 = new int[xd.length];

	public void init(){
		String att = getParameter("dname");
		dname = (att == null) ? "index" : att;
		att = getParameter("dlang");
		dlang = (att == null) ? "" : att;
		bgcolor = new Color(63,182,191);
		ltcolor = new Color(76,224,235);
		dkcolor = new Color(46,140,147);
		cgi = "";
		x0[0] = 0;
		for(int i = 1; i < xd.length; i++){
			x0[i] = x0[i-1] + xd[i-1] +2;
		}
		for(int i = 0; i < xd.length; i++){
			if(dname.equals(bname[i])){
				i0 = i;
			}
		}
		try{
			urlsnd = new URL(getDocumentBase(), "click.au");
		}
		catch(MalformedURLException e){
		}
		sound = getAudioClip(urlsnd);
	}

	public void update(Graphics g){
		paint(g);
	}
    
	public void paint(Graphics g){
		g.setColor(bgcolor);
		g.fillRect(0, 0, 600, 20);
		try{
			menu = new URL(getDocumentBase(), "menu"+dlang+".gif");
		}
		catch(MalformedURLException e){
		}
		image = getImage(menu);
		tracker.addImage(image,0);
		try{
			tracker.waitForID(0);
		}
		catch(InterruptedException e){
		}
		if (tracker.checkID(0)){
			g.drawImage(image, x0[0], 0, this);
		}
		if (i0!=99){
			g.setColor(dkcolor);
			g.fillRect(x0[i0], 0, xd[i0]-1, 2);
			g.fillRect(x0[i0]+xd[i0]-1, 0, 1, 1);
			g.fillRect(x0[i0], 0, 2, 18);
			g.fillRect(x0[i0], 18, 1, 1);
			g.setColor(ltcolor);
			g.fillRect(x0[i0]+1, 18, xd[i0]-1, 2);
			g.fillRect(x0[i0], 19, 1, 1);
			g.fillRect(x0[i0]+xd[i0]-2, 2, 2, 18);
			g.fillRect(x0[i0]+xd[i0]-1, 1, 1, 1);
		}
	}

	public void start(){
		tracker = new MediaTracker(this);
	}

	public boolean mouseEnter(java.awt.Event evt, int x, int y){
		if(dlang.equals("")){
			showStatus("Push a button to go to another English page. Click the pushed button to jump to the Japanese page.");
		}else{
			showStatus("Push a button to go to another Japanese page. Click the pushed button to jump to the English page.");
		}
		return true;
	}

	public boolean mouseDown(java.awt.Event evt, int x, int y){
		requestFocus();
		for(int i = 0; i < xd.length; i++){
			if((x >= x0[i]) && (x <= (x0[i] + xd[i]))){
				i0 = i;
			}
		}
		repaint();
		sound.play();
		return true;
	}

	public boolean mouseUp(java.awt.Event evt, int x, int y){
		if(dname.equals("index")){
			cgi = "count.cgi?";
		}
		if(dname.equals(bname[i0])){
			dlang = (dlang.equals("")) ? "_j" : "";
			cgi = "";
		}
		if(bname[i0].equals("index")){
			   extension = ".php";
		}else{
			   extension = ".html";
		}
		try{
			anchor = new URL(getDocumentBase(), cgi+bname[i0]+dlang+extension);
		}
		catch(MalformedURLException e){
		}
		getAppletContext().showDocument(anchor);
		return true;
	}
}

複数のボタンのどれが押されたかを認識するために15~17行目でbname、xd、x0という3つの配列を用意しています。要素数はどれもボタンの数と同じで、bnameはボタンのリンク先のページ名、xdはボタンの幅(単位はpixel)です。x0は28~31行目で計算しており、各ボタンの左端のx座標です。32~36行目で現在表示中のページ(ページ名dnameはパラメータとして取得)に対応する配列の添字をi0としています。文字列は参照型のため、格納されている値が等しいかどうは Stringクラスのequalsメソッドで判定します。 45行目のupdate()メソッドでpaint()を実行し、このi0を使って現在のページのボタンが押されているように表示します。

94行目からのmouseDown()メソッドでは、requestFocus()でカーソル位置(int x, int y)を取得し、その位置に対応する添字を新たなi0としてrepaint()します。106行目からのmouseUp()メソッドではbname[i0]にジャンプしますが、トップページからジャンプする時はcount.cgi経由にしてアクセスカウンタをカウントアップします(107~109行目)。dlangは19行目からのinit()メソッドで取得した現在のページの言語(英語なら""、日本語なら"_j")で、表示中のページのボタンが押された場合はこれを反転します(110~113行目)。ジャンプ先がトップページの場合のみ拡張子をphpにして、それ以外はhtmlにします(114~118行目)。

2.4 Projector.java

Projector.classはともちゃんテレビを表示するためのものです。JS版ではスライドショーで表示されますが、アプレット版ではテレビの画面をクリックすると82行目からのmouseDown()メソッドでframeをカウントアップして次の静止画を表示するという仕組みでした。静止画1枚の表示にも時間がかかるので、「表示されたら次をクリックしてね」としていた訳です。次の静止画をダウンロードしている間、paint()メソッド内の39行目でテレビ画面には"Wait....."が表示されます。60~68行目が静止画の表示で、photo0.gifはブルー画面のテレビ本体、photo1.gif以降が画面の画像です。


import java.awt.*;
import java.applet.*;
import java.lang.Math;
import java.net.URL;
import java.net.MalformedURLException;

public class Projector extends java.applet.Applet
	{
	URL url;
	Image image;
	Color color;
	Font font;
	MediaTracker tracker;
	String photo, gtype;
	int frame, nframe;

	public void init()
		{
		String att = getParameter("nframe");
		nframe = (att == null) ? 2 : (Integer.valueOf(att).intValue());
		att = getParameter("gtype");
		gtype = (att == null) ? "gif" : att;
		frame = 0;
		color = new Color(255, 0, 0);
		font = new Font("Helvetica", Font.BOLD, 36);
		}

	public void update(Graphics g)
		{
		paint(g);
		}
    
	public void paint(Graphics g)
		{
		if(frame != 0)
			{  
			g.setColor(color);
			g.setFont(font);
			g.drawString("Wait.....", 200, 55);
			}
		photo = "photo" + frame + "." + gtype;
		try
			{
			url = new URL(getDocumentBase(), "photos/" + photo);
			}
		catch(MalformedURLException e)
			{
			}
		image = getImage(url);
		tracker.addImage(image,0);
		try
			{
			tracker.waitForID(0);
			}
		catch(InterruptedException e)
			{
			}
		if (tracker.checkID(0))
			{
			if(frame == 0)
				{
				g.drawImage(image, 0, 0, this);
				}
			else
				{
				g.drawImage(image, 20, 20, this);
				}
			}
		}

	public void start()
		{
		tracker = new MediaTracker(this);
		}

    public boolean mouseEnter(java.awt.Event evt, int x, int y)
		{
		showStatus("Click to show the next picture.");
		return true;
		}

    public boolean mouseDown(java.awt.Event evt, int x, int y)
		{
		frame++;
		if(frame == nframe + 1)
			{
			frame = 1;
			}
		repaint();
	return true;
		}
	}

2.5 Player.java

Player.classは大阪の公園めぐりでアニメーションを表示するためのものです。画像をコマ送りするアニメーションはJavaアプレットの1丁目1番地なので、サンプルコードがそのまま使えました。ですから逆に、コードの詳細については駄待ち狐自身あまりよく分っていません。サウンドラックを付けることも、何回か繰返したら別のページにジャンプすることも、元々のサンプルコードに入っていたと思います。アニメの原画は駄待ち狐の配偶者がArt School Dabblerというお絵描きソフトとペンタブレットで描きました。駄待ち狐がPhotoshopで中割りを作成してアニメは完成です。BGMはMIDIGraphyという簡易作曲ソフトで作曲しました。


import java.awt.*;
import java.awt.image.ImageProducer;
import java.applet.Applet;
import java.applet.AudioClip;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;
import java.net.URL;
import java.net.MalformedURLException;

public class Player extends Applet implements Runnable
	{
	public static final int defaultPause = 300;
	static final String imageLabel = "image";
	static final String soundLabel = "sound";
	static final int STARTUP_ID    = 0;
	static final int ANIMATION_ID  = 1;
	URL hrefURL = null;
	URL imageSource = null;
	URL startUpImageURL = null;
	URL soundSource = null;
	URL soundtrackURL = null;
	Thread engine = null;
	MediaTracker tracker;
	Graphics offScrGC;
	Vector images = null;
	Hashtable durations = null;
	Hashtable sounds = null;
	Image startUpImage = null;
	Image offScrImage;
	AudioClip soundtrack = null;
	Color backgroundColor = getBackground() ;
	Integer frameNumKey;
	int repeatnum, repeat;
	int appWidth = 0;
	int appHeight = 0;
	int frameNum;
	int globalPause = defaultPause;
	boolean userPause = false;
	boolean imageLoadError = false;
	boolean loaded = false;
	boolean error = false;

	final int setFrameNum(int newFrameNum)
		{
		frameNumKey = new Integer(frameNum = newFrameNum);
		return frameNum;
		}
    
	Vector parseImages(String attr, String pattern) throws MalformedURLException
		{
		if(pattern == null)
			{
			pattern = "T%N.gif";
			}
		Vector result = new Vector(10);
		for(int i = 0; i < attr.length(); )
			{
			int next = attr.indexOf('|', i);
			if(next == -1)
				{
				next = attr.length();
				}
			String file = attr.substring(i, next);
			result.addElement(new URL(imageSource, doSubst(pattern, file)));
			i = next + 1;
			}
		return result;
		}

	Hashtable parseSounds(String attr, Vector images) throws MalformedURLException
		{
		Hashtable result = new Hashtable();
		int imageNum = 0;
		int numImages = images.size();
		for(int i = 0; i < attr.length(); )
			{
			if(imageNum >= numImages)
				{
				break;
				}
			int next = attr.indexOf('|', i);
			if(next == -1)
				{
				next = attr.length();
				}
			String sound = attr.substring(i, next);
			if(sound.length() != 0)
				{
				result.put(new Integer(imageNum), new URL(soundSource, sound));
				}
			i = next + 1;
			imageNum++;
			}
		return result;
		}

	Hashtable parseDurations(String attr, Vector images)
		{
		Hashtable result = new Hashtable();
        int imageNum = 0;
        int numImages = images.size();
        for(int i = 0; i < attr.length(); )
            {
            if (imageNum >= numImages) break;
            int next = attr.indexOf('|', i);
            if (next == -1)
                {
                next = attr.length();
                }
            if (i != next)
                {
                int duration = Integer.parseInt(attr.substring(i, next));
                result.put(new Integer(imageNum), new Integer(duration));
                }
            else
                {
                result.put(new Integer(imageNum), new Integer(globalPause));
                }
                i = next + 1;
                imageNum++;
            }
        return result;
        }

	String doSubst(String inStr, String theInt)
		{
        String padStr = "0000000000";
        int length = inStr.length();
        StringBuffer result = new StringBuffer(length);
        for(int i = 0; i < length;)
			{
			char ch = inStr.charAt(i);
			if(ch == '%')
				{
				i++;
				if(i == length)
					{
					result.append(ch);
					}
				else
					{
					ch = inStr.charAt(i);
					if(ch == 'N')
						{
						result.append(theInt);
						i++;
						}
					else
						{
						int pad;
						if ((pad = Character.digit(ch, 10)) != -1)
							{
							String numStr = theInt;
							String scr = padStr+numStr;
							result.append(scr.substring(scr.length() - pad));
							i++;
							}
						else
							{
							result.append(ch);
							i++;
							}
						}
					}
				}
			else
				{
				result.append(ch);
				i++;
				}
			}
		return result.toString();
		}	

	Vector prepareImageRange(int startImage, int endImage, String pattern) throws MalformedURLException
		{
		Vector result = new Vector(Math.abs(endImage - startImage) + 1);
		if (pattern == null)
			{
			pattern = "T%N.gif";
			}
		if(startImage > endImage)
			{
			for(int i = startImage; i >= endImage; i--)
				{
				result.addElement(new URL(imageSource, doSubst(pattern, i+"")));
				}
			}
		else
			{
			for(int i = startImage; i <= endImage; i++)
				{
				result.addElement(new URL(imageSource, doSubst(pattern, i+"")));
				}
			}
		return result;
		}

	Color decodeColor(String s)
		{
		int val = 0;
		try
			{
			if(s.startsWith("0x"))
				{
				val = Integer.parseInt(s.substring(2), 16);
				}
			else if(s.startsWith("#"))
				{
				val = Integer.parseInt(s.substring(1), 16);
				}
			else if(s.startsWith("0") && s.length() > 1)
				{
				val = Integer.parseInt(s.substring(1), 8);
				}
			else
				{
				val = Integer.parseInt(s, 10);
				}
			return new Color(val);
			}
		catch(NumberFormatException e)
			{
			return null;
			}
		}

	public void init()
		{
		tracker = new MediaTracker(this);
		appWidth = size().width;
		appHeight = size().height;
		try
			{
			String href = getParameter("href");
			if(href != null)
				{
				try
					{
					hrefURL = new URL(getDocumentBase(), href);
					}
				catch(MalformedURLException e)
					{
					}
				}
			String param = getParameter("imagesource");	
			imageSource = (param == null) ? getDocumentBase() : new URL(getDocumentBase(), param + "/");
			param = getParameter("startup");
			if(param != null)
				{
				startUpImageURL = new URL(imageSource, param);
				}
			int startImage = 1;
			int endImage = 1;
			param = getParameter("endimage");
			if(param != null)
				{
				endImage = Integer.parseInt(param);
				param = getParameter("startimage");
				if(param != null)
					{
					startImage = Integer.parseInt(param);
					}
				param = getParameter("namepattern");
				images = prepareImageRange(startImage, endImage, param);
				}
			else
				{
				param = getParameter("startimage");
				if(param != null)
					{
					startImage = Integer.parseInt(param);
					param = getParameter("namepattern");
					images = prepareImageRange(startImage, endImage, param);
					}
				else
					{
					param = getParameter("images");
					if(param == null)
						{
						showStatus("No legal IMAGES, STARTIMAGE, or ENDIMAGE "+ "specified.");
						return;
						}
					else
						{
						images = parseImages(param, getParameter("namepattern"));
						}
					}
				}
			param = getParameter("backgroundcolor");
			if(param != null)
				{
					backgroundColor = decodeColor(param);
				}
            param = getParameter("pause");
            globalPause = (param != null) ? Integer.parseInt(param) : defaultPause;
            param = getParameter("pauses");
            if (param != null)
                {
                durations = parseDurations(param, images);
                }
            param = getParameter("repeat");
            repeatnum = (param == null) ? 2 : Integer.parseInt(param);
            repeat = repeatnum;
            param = getParameter("soundsource");
            soundSource = (param == null) ? imageSource : new URL(getDocumentBase(), param + "/");
            param = getParameter("sounds");
                if(param != null)
                    {
                    sounds = parseSounds(param, images);
                    }
                param = getParameter("soundtrack");
                if (param != null)
                    {
                    soundtrackURL = new URL(soundSource, param);
                    }
                }
            catch(MalformedURLException e)
                {
                }
            setFrameNum(0);
        }

	void tellLoadingMsg(String file, String fileType)
		{
        showStatus("Animator: loading "+fileType+" "+file);
		}

	void tellLoadingMsg(URL url, String fileType)
		{
        tellLoadingMsg(url.toExternalForm(), fileType);
		}

	void clearLoadingMessage()
		{
        showStatus("");
		}

	boolean fetchImages(Vector images)
		{
        int i;
        int size = images.size();
        for(i = 0; i < size; i++)
			{
			Object o = images.elementAt(i);
			if(o instanceof URL)
				{
               URL url = (URL)o;
                tellLoadingMsg(url, imageLabel);
               Image im = getImage(url);
               tracker.addImage(im, ANIMATION_ID);
               images.setElementAt(im, i);
				}
			}
		try
			{
			tracker.waitForID(ANIMATION_ID);
			}
		catch (InterruptedException e)
			{
			}
		if(tracker.isErrorID(ANIMATION_ID))
			{
			return false;
			}
		return true;
		}

	URL fetchSounds(Hashtable sounds)
		{
        for(Enumeration e = sounds.keys() ; e.hasMoreElements() ;)
            {
			Integer num = (Integer)e.nextElement();
			Object o = sounds.get(num);
			if (o instanceof URL)
				{
                URL file = (URL)o;
                tellLoadingMsg(file, soundLabel);
                try
					{
					sounds.put(num, getAudioClip(file));
					}
				catch (Exception ex)
					{
					return file;
					}
				}
			}
		return null;
		}

	void startPlaying()
		{
		if(soundtrack != null)
			{
            soundtrack.play();
			}
		}

	void stopPlaying()
		{
		if (soundtrack != null)
			{
            soundtrack.stop();
			}
		}

	public void run()
		{
        Thread me = Thread.currentThread();
        URL badURL;
        me.setPriority(Thread.MIN_PRIORITY);
        if(!loaded)
			{
			try
				{
                if(startUpImageURL != null)
					{
                    tellLoadingMsg(startUpImageURL, imageLabel);
                    startUpImage = getImage(startUpImageURL);
                    tracker.addImage(startUpImage, STARTUP_ID);
                    tracker.waitForID(STARTUP_ID);
					repaint();
					}
				if(!fetchImages(images))
					{
                    Object errors[] = tracker.getErrorsAny();
                    Image im = (Image)errors[0];
                    return;
					}
				if(soundtrackURL != null && soundtrack == null)
					{
                    tellLoadingMsg(soundtrackURL, imageLabel);
                    soundtrack = getAudioClip(soundtrackURL);
					}
				if (sounds != null)
					{
                    badURL = fetchSounds(sounds);
					}
				clearLoadingMessage();
                offScrImage = createImage(appWidth, appHeight);
                offScrGC = offScrImage.getGraphics();
                offScrGC.setColor(Color.lightGray);
                loaded = true;
                error = false;
				}
			catch(Exception e)
				{
                error = true;
                e.printStackTrace();
				}
			}
		if(userPause)
			{
			return;
			}
		startPlaying();
		try
			{
            if(images.size() > 1)
				{
                while(appWidth > 0 && appHeight > 0 && engine == me)
					{
                    if(frameNum >= images.size())
						{
						setFrameNum(0);
						if(--repeat != 0)
							{
							startPlaying();
							}
						}
					if(repeat == 0)
						{
						if(hrefURL != null)
							{
							repeat = repeatnum;
							getAppletContext().showDocument(hrefURL);
							}
						else
							{
							engine.stop();
							}
						}
					else
						{
						repaint();
						}
                    if(sounds != null)
						{
                        AudioClip clip = (AudioClip)sounds.get(frameNumKey);
                        if(clip != null)
							{
                            clip.play();
							}
						}
					try
						{
                        Integer pause = null;
                        if(durations != null)
							{
                            pause = (Integer)durations.get(frameNumKey);
							}
                        if(pause == null)
							{
                            Thread.sleep(globalPause);
							}
						else
							{
                            Thread.sleep(pause.intValue());
							}
						}
					catch (InterruptedException e)
						{
						}
					setFrameNum(frameNum+1);
					}
				}
			}
		finally
			{
            stopPlaying();
			}
		}

	public void update(Graphics g)
		{
        paint(g);
		}

	public void paint(Graphics g)
		{
        if(error || !loaded)
			{
            if (startUpImage != null)
				{
                if(tracker.checkID(STARTUP_ID))
					{
                    g.drawImage(startUpImage, 0, 0, this);
					}
				}
			else
				{
				g.clearRect(0, 0, appWidth, appHeight);
				}
			}
		else
			{
		if((images != null) && (images.size() > 0) && tracker.checkID(ANIMATION_ID))
				{
                if(frameNum < images.size())
					{
					offScrGC.clearRect(0, 0, appWidth, appHeight);
					Image image = (Image)images.elementAt(frameNum);    
					if(backgroundColor != null)
						{
                        offScrGC.setColor(backgroundColor);
                        offScrGC.fillRect(0, 0, appWidth, appHeight);
                        offScrGC.drawImage(image, 0, 0, backgroundColor, this);
						}
					else
						{
                        offScrGC.drawImage(image, 0, 0, this);
						}
					g.drawImage(offScrImage, 0, 0, this);
					}
				else
					{
					g.fillRect(0, 0, appWidth, appHeight);
					g.drawImage((Image)images.lastElement(), 0, 0, this);
					}
				}
			}
		}

	public void start()
		{
        if(engine == null)
            {
            engine = new Thread(this);
            engine.start();
			}
		showStatus(getAppletInfo());
		}

	public void stop()
		{
        if(engine != null && engine.isAlive())
			{
            engine.stop();
			}
		engine = null;
		}

	public boolean handleEvent(Event evt)
		{
        switch(evt.id)
			{
			case Event.MOUSE_DOWN:
            if(loaded)
                {
                if(engine != null && engine.isAlive())
                    {
                    if(userPause)
                        {
                        engine.resume();
                        }
                    else
                        {
                        stopPlaying();
                        engine.suspend();
                        }
                    userPause = !userPause;
                    }
                else
                    {
                    userPause = false;
                    setFrameNum(0);
                    engine = new Thread(this);
                    engine.start();
                    }
                }
            return true;
            case Event.MOUSE_UP:
            case Event.MOUSE_ENTER:
            showStatus("Click to stop/restart.");
            return true;
            case Event.MOUSE_EXIT:
            showStatus("");
            return true;
            case Event.KEY_ACTION:
            case Event.KEY_RELEASE:
            case Event.KEY_ACTION_RELEASE:
            default:
            return super.handleEvent(evt);
			}
		}
	}

2.6 Pointer.java

Pointer.classは大阪の公園めぐりでガイドマップを表示するためのものです。地図上に公園の位置を表示しておき、クリックでそのページにジャンプするだけならクリッカブルマップを使えば済む話です。ただ、ここにも駄待ち狐のこだわりがあって、リストにある公園名をクリックしたら地図上にその位置が表示されるという「ランプ点灯式観光マップ」みたいにしたかったのです。さらに、リストで別の公園をクリックすると元のランプは色が変って、新しい公園のランプが点灯する、最後に点灯した地図上のランプをクリックするとその公園のページにジャンプするという仕掛けにしました。


import java.awt.*;
import java.applet.*;
import java.lang.Math;
import java.net.URL;
import java.net.MalformedURLException;

public class Pointer extends java.applet.Applet
	{
	URL map, help2, anchor;
	Image image;
	Color red, green, blue;
	Font font;
	MediaTracker tracker;
	int ppnum, pnum;
	int [] xp = {272,264,376,402,364,331,382,265,276,304,336,326,355,238,170,234,145,96};
	int [] yp = {197,132,209,163,229,315,281,332,332,363,453,425,398,385,466,465,489,537};

	public void init()
		{
		red = new Color(255,0,0);
		green = new Color(0,255,0);
		blue = new Color(0,0,255);
		font = new Font("Helvetica", Font.BOLD, 36);
		ppnum = 0;
		pnum = 0;
		}

	public void update(Graphics g)
		{
		paint(g);
		}

	public void paint(Graphics g)
		{
		if(pnum == 0)
			{
			g.setColor(red);
			g.setFont(font);
			g.drawString("Wait.....", 260, 70);
			try
				{
				map = new URL(getDocumentBase(), "guides/map.gif");
				}
			catch(MalformedURLException e)
				{
				}
			image = getImage(map);
			tracker.addImage(image,0);
			try
				{
				tracker.waitForID(0);
				}
			catch(InterruptedException e)
				{
				}
			if (tracker.checkID(0))
				{
				g.drawImage(image, 0, 0, this);
				}
			}
		if(pnum > 0)
			{
			g.setColor(red); 
			g.fillOval(xp[pnum-1]-7, yp[pnum-1]-7, 15, 15);
			g.fillOval(15, 64+(pnum-1)*18, 12, 12);
			if(ppnum == 0)
				{
				try
					{
					help2 = new URL(getDocumentBase(), "guides/help2.gif");
					}
				catch(MalformedURLException e)
					{
					}
				image = getImage(help2);
				tracker.addImage(image,1);
				try
					{
					tracker.waitForID(1);
					}
				catch(InterruptedException e)
					{
					}
				if (tracker.checkID(1))
					{
					g.drawImage(image, 262, 4, this);
					}	  
				}
			if(ppnum > 0)
				{
				g.setColor(green);
				g.fillOval(xp[ppnum-1]-7, yp[ppnum-1]-7, 15, 15);
				g.fillOval(15, 64+(ppnum-1)*18, 12, 12);
				}
			if(ppnum < 0)
				{
				g.setColor(red); 
				g.fillOval(xp[pnum-1]-11, yp[pnum-1]-11, 23, 23);
				g.setColor(green); 
				g.fillOval(xp[pnum-1]-7, yp[pnum-1]-7, 15, 15);
				ppnum = 0;
				pnum = 0;
				}
			}
		}

	public void start()
		{
		tracker = new MediaTracker(this);
		}

    public boolean mouseEnter(java.awt.Event evt, int x, int y)
		{
		showStatus("");
		return true;
		}

    public boolean mouseDown(java.awt.Event evt, int x, int y)
		{
		requestFocus();
		if((x >=10) && (x <=200) && (y >= 61) && (y < 385))
			{
			if(pnum != ((y-61)/18+1))
				{
				ppnum = pnum;
				pnum = (y-61)/18+1;
				}
			repaint();
			}
		if(pnum > 0)
			{
			if((x >= xp[pnum-1]-7) && (x <= xp[pnum-1]+7) && (y >= yp[pnum-1]-7) && (y <= yp[pnum-1]+7))
				{
				int urlnum = pnum;
				ppnum = -1;
				repaint();
				try
					{
					anchor = new URL(getDocumentBase(), "guides/park"+urlnum+"_j.html");
					}
				catch(MalformedURLException e)
					{
					}
				getAppletContext().showDocument(anchor);
				}
			}
	return true;
		}
	}

上記の動作はpnum (park number)とppnum (previous pnum)によって制御します。いずれも初期値は0です。まず、28行目のupdate()メソッドで初期画面を出しますが、この時は33行目のpaint()メソッドで35~60行目が実行されます。map.gifは22.6KBしかありませんが、それでもすぐには表示されない場合があったので"Wait....."を表示して待ちます。次に、118行目からのmouseDownメソッドで公園のリスト領域がクリックされると、126行目で何番目の公園かを計算してpnumとします。123行目のif文で、同じ公園をクリックした場合は何もしません。クリックが2回目以降だった場合は、ppnumに前回のpnumを保存します。

128行目のrepaint()で再度paint()を実行すると、63~65行目の処理となります。64行目で公園の位置に赤丸を表示しますが、公園の座標は15~16行目で(xp, yp)として設定済みです。65行目はリストの番号に赤丸を付けます。66~88行目は最初に公園をクリックした場合で、地図の右上の説明文が変ります。89~94行目はクリックが2回目以降だった場合で、前回クリックした公園の赤丸を黄緑色に変えます。地図上の赤丸をクリックした場合は、132~145行目でppnumを-1に設定してrepaint()することで95~103行目が実行され、地図上の赤丸を大きな赤丸の中に黄緑色の丸があるように変えた後、公園のページにジャンプします。

JavaアプレットをJavaScriptで代替(2)へ続く