補講B グラフィックス
シラバスでは第6講で実施する予定でしたが,以下の理由により実施しないことにしました. 講義資料はそのまま置いておきます.
- グラフィックスで利用している
EZ.javaの配布元が配布をやめた.- GitHub では公開されているようです.
- devcontainer を利用することになり,Docker 上の GUI をmacOS上に表示するためにトリッキーな方法を利用せざるを得ない.
- できることはできるようですが,ややこしいので授業としては扱いません.
シラバスでは第6講で実施する予定でしたが,以下の理由により実施しないことにしました. 講義資料はそのまま置いておきます.
EZ.java の配布元が配布をやめた.
EZ.java本日のプログラムを実行するには EZ.javaが必要です.
これから書くプログラムと,同じディレクトリにこのEZ.javaを置いてください.
EZ.java は正式には,EZ Graphics と呼びます.
このプログラムは,ハワイ大学マノア校の Advanced Visualization and Applications 研究室 Dylan Kobayashi によって開発されたソフトウェアです.
1 つのファイルを同じディレクトリに置くことで図形の描画が容易に扱えるようになります.
公式サイトからもダウンロードできますが,
この授業向けに少し修正していますので,ここからEZ.javaをダウンロードして利用してください.
ダウンロードしたら,早速プログラムを書いていきましょう. 書き終えたら,コンパイル,実行してみましょう. 実行結果を確認できれば,値を変更して再度,コンパイル,実行してみましょう. どこを変更すると,どう変わるのかを確認してください.
import java.awt.Color;
public class DrawOval{
void run(){
EZ.initialize(400, 400); // 画面の大きさを決める.
// 円を描く.(中心座標x, y, 幅,高さ,色,塗りつぶし)
EZCircle circle1 = EZ.addCircle(
100, 100, 200, 200, Color.BLUE, true);
EZCircle circle2 = EZ.addCircle(
200, 200, 200, 200, Color.RED, false);
}
// mainメソッドは省略.
}
なお,Colorは色を表す型です.この型を利用するときには,import java.awt.Color;の一文がクラス宣言の前に必要です.
そして,BLACK, BLUE, CYAN, DARK_GRAY, GRAY, GREEN, LIGHT_GRAY
, MAGENTA, ORANGE, PINK, RED, WHITE, YELLOW の 13 色が定義済みです.
座標は左右方向が x 軸,上下方向が y 軸になっており,右側が x 軸のプラス方向,左端が x 軸の 0 です. また,上下方向では,一番上が y 方向の 0,下方向がプラスになっていることに注意してください.
円以外の図形も描いてみましょう. どのようなメソッドを呼び出せば良いかは,EZ Graphics のドキュメント を読んでみましょう.
https://www2.hawaii.edu/~dylank/ics111/doc/

EZ Graphics ドキュメント1

EZ Graphics ドキュメント2
各ページの左のフレームは,EZ Graphics が持つ型を表しています. 右のフレームは,その型が持つ情報を表しています.右のフレームを下にスクロールしていくと, メソッドの一覧が閲覧できます(上の右側の画像).
この形式のドキュメントを一般に API ドキュメント と呼びます. Javadoc コメントで生成されるドキュメントです.
では,EZ Graphics の API ドキュメントを参照し, 楕円以外の図形も描いてみましょう.
まず,DrawOval.javaをコピーしてDrawShapes.javaを作成してください.
そして,DrawShapes.javaに追加・変更していってください.
なお,DrawShapes.javaのmainメソッドの中身も忘れずに変更しましょう.
アニメーションは基本的にパラパラ漫画と同じ原理で行います.
EZ Graphics では,EZ.addCircleやEZ.addLineなどで
追加して返される実体の位置を変更し,EZ.refreshScreen()を呼び出すと画面を更新します.
また,Java で一定時間スリープするのは,Thread.sleepメソッドを利用します. sleepメソッドに
スリープする時間(ミリ秒)を渡します.
次の例では,100 ミリ秒(0.1 秒)スリープしています.
次の例で確認してみましょう.
import java.awt.Color;
public class RoundTrip{
void run(){
EZ.initialize(400, 400);
EZCircle circle = EZ.addCircle(
100, 100, 5, 5, Color.BLUE, true);
this.roundTrip(circle);
}
void roundTrip(EZCircle circle){
Integer deltaX = 10;
while(true){ // 無限ループ
Integer newX = circle.getXCenter() + deltaX;
circle.translateTo(newX, circle.getYCenter());
if(newX >= 400 || newX <= 0){
deltaX = deltaX * -1;
}
EZ.refreshScreen();
Thread.sleep(100);
}
}
}これでコンパイルすると,次のようなコンパイルエラーが発生します.
RoundTrip.java:17: エラー: 例外InterruptedExceptionは報告されません。スローするには、捕捉または宣言する必要があります
Thread.sleep(100);
^
エラー1個例外(Exception)は,近年のプログラミング言語で採用されている実行時エラーの通知機構です.
従来のプログラミング言語,例えば,C 言語の場合,fopenで開くファイルが見つからなかった場合,
返り値をNULL にすることでエラーを通知していました. この場合,プログラマが責任を持って,
返り値の値を確認して,正常か異常かを判断しなければいけませんでした.
一方の例外機構は,異常処理を行うための別の処理経路を作るものです. もし,プログラムの実行途中で何らかの異常が発生した時,それまでに行なっていた処理を中断し, 別の処理を行うようにする機構です.
graph LR;
A[ファイルを開く] --> B{存在確認}
B -->|存在する| C[正常処理]
B -->|存在しない| D[異常処理]
C 言語のようなエラー処理は上記のフローチャートのように,
プログラマ自身がfopenの返り値を元に分岐処理によって,正常処理,異常処理を振り分ける必要があります.
graph LR; A[ファイルを開く] A --> C[正常処理] A -->|存在しない| D[異常処理]
対して,例外機構がサポートされている言語の場合,プログラマが異常,正常の分岐を明示的に書く必要はありません. 異常が起こった場合の処理の経路が決まっており,その経路に処理を書いておくことが異常処理を行うことになります. 正常処理の場合は,それまでの処理の続きにそのまま処理を書いていきます. これにより正常処理の見通しがよくなります.
そして,例外が投げられれば,何らかの異常が発生したと判断できるようになります. 逆に,例外が投げられなければ,データが変であろうが,正常な処理であると判断できます (もちろん,開発途中で変な場合はバグの可能性はありますが).
プログラマが明示的に投げる例外も存在しますが,多くの場合,システムが異常を検知し例外を投げます. 例えば,ファイルが見つからない場合,スリープ中に割り込みが入った場合などです. そのような例外が発生した時に,どのような対応をするのかをあらかじめ決めておく必要があります.
次のプログラムが,例外処理のイメージです.
void exceptionalMethod() throws Exception {
// 例外が発生する可能性のある処理
someMethodCall();
// 例外が起こらなかった場合の処理
process();
}someMethod の実行中に何らかの例外が起こった場合,まずsomeMethodの呼び出し元であるexceptionalMethodにどのように処理するかが問い合わされます.
そして,exceptionalMethodは例外は処理せず,呼び出し元に処理を任せるよう設定している(メソッドにthrows Exceptionと宣言しているため)ため,exceptionalMethod の呼び出し元に通知されます.
このthrows節は後ほど説明します.
Java の例外は,検査例外と非検査例外の2つに分類できます. 違いは次に挙げる通りです.
例外が発生した時の処理をプログラム中に明示的に書いておかなければコンパイルエラーになる例外. 完全に防ぐことが不可能な例外(実行時の状態によって発生しうる例外).
例えば,実行時エラーは,プログラムでどれほど厳密にチェックしたとしても,完全に回避できません.
IOException
InterruptedException
一方,非検査例外はプログラム中で十分にチェックすることで避けることが可能です. そのため,例外が発生した時の処理は書かなくてもコンパイルが通るようになっています.
NullPointerException
ArrayIndexOutOfBoundsException
NumberFormatException
非検査例外は,事前にチェックすることで,例外の発生を抑えられます. 逆に言えば,実行時に非検査例外が投げられた場合は,事前のチェックが不十分であるとも言えます. 例えば,配列の範囲を超えてアクセスすることは,事前に配列の範囲を超えないようにプログラム中で確認することで避けられます.
さて,例外が発生した時の対処法をプログラム中に書いておく必要があります. 取れる対処法は2つです.
どちらがふさわしいかは場合により異なります. ここでは,呼び出し元に対応を任せましょう. そのために,呼び出し元に責任を丸投げするようにプログラム中に明示しておきましょう.
こうすることで,例外が発生した時はそのメソッドの呼び出し元に責任を転嫁し,正常処理のみに集中して処理を書くことができます.
本講義では,例外に対応する処理については省略します.
詳細を知りたい場合は,try-catch について調べてみてください.
さて,コンパイルエラーで,InterruptedExceptionが投げられる可能性があると述べられています.
この例外は,スリープ中に割り込みが発生した時に発生する例外です.
ここでは,呼び出し元に責任を転嫁しましょう.
以下のような対応になります.
Thread.sleep が例外を発生させた時,Thread.sleep
の呼び出し元であるroundTripに対応が任されます.これが例外が投げられた,ということです.roundTripの呼び出し元であるrunに対応を任せましょう.runでも,呼び出し元であるmainに処理を任せる事にします.mainも呼び出し元に対応を任せましょう.呼び出し元に対応を任せるには,メソッドのシグネチャに throws節を追加します.
throws 節は以下のように指定します.
以下のように書くことで,例外クラスが発生した時,methodNameの呼び出し元に責任を転嫁することができるようになります.
public class ClassName{
void methodName() throws 例外クラス {
// 検査例外が発生する可能性のある処理
}
}なお,複数の例外が発生する可能性のある場合,throws節に例外クラスの名前をコンマ区切りで指定できます.
すなわち,メソッドの宣言部分を以下のように変更してください.
public class RoundTrip{
void run() throws InterruptedException{
// ... 省略
// roundTripで IntrruptedException が発生する可能性がある.
this.roundTrip(circle);
}
void roundTrip(EZCircle circle)
throws InterruptedException{
// .... 省略
// Thead.sleepの呼び出しで
// IntrruptedExceptionが発生する可能性がある.
Thread.sleep(100);
}
public static void main(String[] args) throws InterruptedException{
RoundTrip trip = new RoundTrip();
// runでIntrruptedExceptionが発生する可能性がある.
trip.run();
}
}このようにプログラムを変更し,コンパイルしてみましょう.今度はコンパイルできたはずです. 次のような実行結果となるはずです.

このように,アニメーションを行うには,スリープが必要です. 一方,スリープを行うには,例外への対応が必要になります. 今後,ファイルの入出力を扱う時にも例外機構は必要になってきますので, どのような機構であるのか,しっかりと押さえておいてください.
スリープを行わないと,環境によっては目にも留まらぬ速さでアニメーションが繰り広げられます.
以下の実行結果になるよう,鉛直投げ上げ運動のアニメーションを作成してみましょう.
(アニメーションが途中で終わっているので変な挙動のように見えますが,バウンドし続ける動きになっています)
クラス名は Bound としてください.
$y$方向の 0 が一番上,下方向がプラスになっていますので,通常の投げ上げとは方向が逆になっています(上方向に重力がかかっていると思ってください). 必要な式は次の通りです.

次の図になるようにプログラムを作成してみましょう.
色は必ずしもこの通りでなくて構いません.
プログラム名は,DrawShapes2としてください.

$0 \leq x \leq 2\pi$ の範囲でサイン波を描画してみましょう.クラス名は SineCurve としてください.

上の図は,399 本の直線で描画されています.
x=0から400未満まで繰り返し,v = Math.sin(i * delta) * s で
$(x_i, y_i)=(i, v)$を得ます.
$((x_{i-1}, y_{i-1}), (x_i, y_i))$に線を追加することで上記のサイン波が描画できます.
なお,delta(
$\delta$)は横幅,s(
$s$)は高さを表しています.
EZ.initialize(400, 400) で初期化した時,
delta が
$\delta = \frac{2\pi}{400}$,sは 150としてください(
$-1 \leq \sin\theta \leq 1$であり,この値を -150〜150に割り当てるため).
また,中央に寄せるため,y 軸方向に +200 としてください.
Java で,
$\sin$, $\cos$を計算するには,Math.sin,Math.cosメソッドを利用しましょう.
ただし,以下の点に注意してください.
Math.PIという変数を利用してください.Double sinValue = Math.sin(Math.PI / 3.0);
Double cosValue = Math.cos(Math.PI / 3.0);Double型をInteger型として扱うには,Double型の変数に対して,intValueメソッドを呼び出しましょう.
例えば,Double型のdValueという値を Integer型のiValueに代入するには,次のようなプログラムになります.
Double dValue = // Double型の値を代入.
Integer iValue = dValue.intValue();クラス名は,KochCurveとしてください.
コッホ曲線の例を以下に示します(画像のクリックで画像が更新されます).
この計算式で,コッホ曲線を描いてみましょう. 再帰呼び出しを利用すると良いでしょう. $(x_2, y_2),(x_3, y_3)$間の直線を引く時,また,$(x_3, y_3),(x_4, y_4)$間の直線を引く時に それぞれを$(x_1, y_1),(x_5, y_5)$として再帰呼び出しを行えばコッホ曲線を描けるでしょう.
次のようなメソッドを用意しましょう.このメソッドを呼び出すことで,2点の間にコッホ曲線を描けるようになります.
void drawKoch(Integer x1, Integer y1, Integer x5, Integer y5,
Integer dimension, Double angle){
if(dimension == 0){
// (x1, y1)から(x5, y5)まで線を引く.
}
else{
// (x1, y1), (x5, y5) 間の長さの 1/3.これが l となる.
Double length = // 長さlを求める.
Double delta = Math.PI / 3.0;
// (x2, y2) を求める.
// (x1, y1)から(x2, y2)まで線を引く.
// (x3, y3) を求める(θ は angle + delta).
// (x2, y2)から(x3, y3)まで線を引く.
this.drawKoch(x2.intValue(), y2.intValue(),
x3.intValue(), y3.intValue(),
dimension - 1, angle + delta);
// (x4, y4) を求める(θ は angle - delta).
// (x3, y3)から(x4, y4)まで線を引く.
this.drawKoch(x3.intValue(), y3.intValue(),
x4.intValue(), y4.intValue(),
dimension - 1, angle - delta);
// (x4, y4)から(x5, y5)まで線を引く.
}
}
画像のクリックで画像が更新されます.
コッホ曲線を
$n=0$から$n=5$までを 1 秒程度で更新して描いてみましょう.
クラス名は,KochCurveAnimationとしてください.
今まで描画した内容を消したい場合は,EZ.removeAllEZElements() メソッドを呼び出してください.
例題 3と例題 4を合わせた動きをするボールを描きましょう.
$x$軸方向には,例題3を,$y$軸方向には,例題4の動きを設定すれば良いでしょう.
クラス名を ThrowingExercise にしてください.

EZ Graphics を利用して,自由にアニメーションを描いてください.
クラス名は Animation としてください.
内容は自由です.
EZ.javaを利用して,アナログ時計を描画してください.
クラス名は Clockとします.
この時の時刻は,19時22分25秒.
Math.toRadians に度数法の値(0〜360度)を渡せばラジアン値が得られる.Double degree = // 度数法による角度
Double radian = Math.toRadians(degree); // 弧度法による角度degreeOfSeconds = date.getSeconds() * 6.0 - 90 で得られる角度を元に計算する.date.getSeconds()の代わりに date.getMinutes() を利用する.degreeOfHours = (date.getHours() * 5 + date.getMinutes() / 12.0) * 6.0 - 90 で値を得る.
date.getHours() * 30 - 90で計算すると,例えば,6:30 の時でも短針は一番下を指したままである.
そうではなく,長針が進むに従って,短針も少しずつ移動して欲しいため,上記の処理としている.EZ.refreshScreen() を呼び出すと,今まで追加した要素を再描画します.
つまり,これまでに addLineやaddCircleなどで追加した図形を再描画します.
ここでは,1秒ごとに新たな線を引きたいため,今まで追加した図形を削除する必要があります.
そのために,EZ.refreshScreen()ではなく,EZ.removeAllEZElements()を呼び出す必要があります.
もちろん,追加した EZLine の実体に対して,適切に座標を変更した上で,EZ.refreshScreen()を呼び出せば期待通りの動作となります.
IOException: 入出力エラーが起こった時.InterruptedException: スリープ中に割り込みが発生した時.NullPointerException: 参照先の実体が存在しない変数に対して操作を行なった時.IndexOutOfBoundsException: 有効な範囲を超えてリストにアクセスしようとした時.NumberFormatException: 文字列を数字に変換する処理で,変換に失敗した時.throws 節を追加する.
返り値の型 メソッド名(メソッドの引数) throws 例外名{ メソッドのボディ }try-catch構文を利用する.