OSDN Git Service

61d01e395cf680a6370acc094366e93514a5e8ac
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / anogakki / AnoGakkiPane.java
1 package camidion.chordhelper.anogakki;
2
3 import java.awt.BasicStroke;
4 import java.awt.Color;
5 import java.awt.Component;
6 import java.awt.Graphics;
7 import java.awt.Graphics2D;
8 import java.awt.Point;
9 import java.awt.Rectangle;
10 import java.awt.Stroke;
11 import java.awt.geom.AffineTransform;
12 import java.util.Iterator;
13 import java.util.LinkedList;
14 import java.util.List;
15
16 import javax.swing.JComponent;
17 import javax.swing.JLayeredPane;
18 import javax.swing.SwingUtilities;
19 import javax.swing.Timer;
20
21 /**
22  * Innocence「あの楽器」風の表示を行う拡張クラスです。
23  *
24  * <p>{@link #start()} メソッドで表示を開始でき、
25  * 時間が経過すると表示が自然に消えるようになっています。
26  * </p>
27  * <p>画面を「あの楽器」化するには、その画面とともに
28  * このクラスのインスタンスを {@link JLayeredPane} で重ね合わせます。
29  * </p>
30  */
31 public class AnoGakkiPane extends JComponent {
32         /**
33          * 1ステップあたりの時間間隔(ミリ秒)
34          */
35         private static final int INTERVAL_MS = 15;
36         /**
37          * 表示終了までのステップ数
38          */
39         private static final int INITIAL_COUNT = 20;
40         /**
41          * 角速度ωの(絶対値の)最大
42          */
43         private static final double MAX_OMEGA = 0.005;
44         /**
45          * 図形の種類
46          */
47         private static enum Shape {
48                 /** ○ */
49                 CIRCLE {
50                         public void draw(Graphics2D g2, QueueEntry entry) {
51                                 entry.drawCircle(g2);
52                         }
53                 },
54                 /** = */
55                 LINES {
56                         public void draw(Graphics2D g2, QueueEntry entry) {
57                                 entry.drawLines(g2);
58                         }
59                 },
60                 /** □ */
61                 SQUARE {
62                         public void draw(Graphics2D g2, QueueEntry entry) {
63                                 entry.drawSquare(g2);
64                         }
65                 },
66                 /** △ */
67                 TRIANGLE {
68                         public void draw(Graphics2D g2, QueueEntry entry) {
69                                 entry.drawTriangle(g2);
70                         }
71                 };
72                 /**
73                  * 図形の種類の値をランダムに返します。
74                  * @return ランダムな値
75                  */
76                 public static Shape randomShape() {
77                         return values()[(int)(Math.random() * values().length)];
78                 }
79                 /**
80                  * この図形を描画します。
81                  * @param g2 描画オブジェクト
82                  * @param entry キューエントリ
83                  */
84                 public abstract void draw(Graphics2D g2, QueueEntry entry);
85         }
86         /**
87          * 色(RGBA、Aは「不透明度」を表すアルファ値)
88          */
89         private static final Color color = new Color(0,255,255,192);
90         /**
91          * 線の太さ
92          */
93         private static final Stroke stroke = new BasicStroke((float)5);
94         /**
95          * いま描画すべき図形を覚えておくためのキュー
96          */
97         private List<QueueEntry> queue = new LinkedList<QueueEntry>();
98         /**
99          * キューエントリ内容
100          */
101         private class QueueEntry {
102                 /** 時間軸 */
103                 private int countdown = INITIAL_COUNT;
104                 /** スタートからの経過時間(ミリ秒) */
105                 private int tms = 0;
106                 //
107                 /** 図形の種類 */
108                 AnoGakkiPane.Shape shape = Shape.randomShape();
109                 /** クリックされた場所(中心) */
110                 private Point clickedPoint;
111                 //
112                 // 回転する場合
113                 /** 現在の半径 */
114                 private int r = 0;
115                 /** 回転速度(時計回り) */
116                 private double omega = 0;
117                 /** 現在の回転角(アフィン変換) */
118                 AffineTransform affineTransform = null;
119                 /**
120                  * 新しいキューエントリを構築します。
121                  * @param clickedPoint クリックされた場所
122                  */
123                 public QueueEntry(Point clickedPoint) {
124                         this.clickedPoint = clickedPoint;
125                         if( shape != Shape.CIRCLE ) {
126                                 // ○以外なら回転角を初期化
127                                 //(○は回転しても見かけ上何も変わらなくて無駄なので除外)
128                                 affineTransform = AffineTransform.getRotateInstance(
129                                         2 * Math.PI * Math.random(),
130                                         clickedPoint.x,
131                                         clickedPoint.y
132                                 );
133                                 omega = MAX_OMEGA * (1.0 - 2.0 * Math.random());
134                         }
135                 }
136                 /**
137                  * このキューエントリをカウントダウンします。
138                  * @return カウントダウン値(0 でタイムアウト)
139                  */
140                 public int countDown() {
141                         if( countdown > 0 ) {
142                                 // 時間 t を進める
143                                 countdown--;
144                                 tms += INTERVAL_MS;
145                                 // 半径 r = vt
146                                 r = tms / 2;
147                                 // 回転
148                                 if( shape == Shape.SQUARE || shape == Shape.TRIANGLE ) {
149                                         // 角度を θ=ωt で求めると、移動距離 l=rθ が
150                                         // t の2乗のオーダーで伸びるため、加速しているように見えてしまう。
151                                         // 一定の速度に見せるために t を平方根にして角度を計算する。
152                                         affineTransform.rotate(
153                                                 omega * Math.sqrt((double)tms),
154                                                 clickedPoint.x,
155                                                 clickedPoint.y
156                                         );
157                                 }
158                         }
159                         return countdown;
160                 }
161                 /**
162                  * ○を描画します。
163                  * @param g2 描画オブジェクト
164                  */
165                 public void drawCircle(Graphics2D g2) {
166                         int d = 2 * r;
167                         g2.drawOval( clickedPoint.x-r, clickedPoint.y-r, d, d );
168                 }
169                 /**
170                  * =を描画します。
171                  * @param g2 描画オブジェクト
172                  */
173                 public void drawLines(Graphics2D g2) {
174                         int width2 = 2 * getSize().width;
175                         int y = clickedPoint.y;
176                         g2.transform(affineTransform);
177                         g2.drawLine( -width2, y-r, width2, y-r );
178                         g2.drawLine( -width2, y+r, width2, y+r );
179                 }
180                 /**
181                  * □を描画します。
182                  * @param g2 描画オブジェクト
183                  */
184                 public void drawSquare(Graphics2D g2) {
185                         int d = 2 * r;
186                         g2.transform(affineTransform);
187                         g2.drawRect( clickedPoint.x-r, clickedPoint.y-r, d, d );
188                 }
189                 /**
190                  * △を描画します。
191                  * @param g2 描画オブジェクト
192                  */
193                 public void drawTriangle(Graphics2D g2) {
194                         int x = clickedPoint.x;
195                         int y = clickedPoint.y;
196                         g2.transform(affineTransform);
197                         g2.drawLine( x-r, y, x+r, y-r );
198                         g2.drawLine( x-r, y, x+r, y+r );
199                         g2.drawLine( x+r, y-r, x+r, y+r );
200                 }
201         }
202         /**
203          * キューを更新するアニメーション用タイマー
204          */
205         public AnoGakkiPane() {
206                 setOpaque(false);
207                 timer = new Timer(INTERVAL_MS, e->{
208                         synchronized(queue) {
209                                 Iterator<QueueEntry> i = queue.iterator();
210                                 while(i.hasNext()) if(i.next().countDown() <= 0) i.remove();
211                         }
212                         if(queue.isEmpty()) timer.stop();
213                         repaint();
214                 }) {
215                         {setCoalesce(true);setRepeats(true);}
216                 };
217         }
218         private Timer timer;
219         @Override
220         public void paint(Graphics g) {
221                 if(queue.isEmpty()) return;
222                 Graphics2D g2 = (Graphics2D)g;
223                 g2.setStroke(stroke);
224                 g2.setColor(color);
225                 synchronized(queue) { queue.forEach(e -> e.shape.draw(g2, e)); }
226         }
227         /**
228          * 指定された長方形領域({@link Rectangle})の中央から図形の表示を開始します。
229          * @param source AWTコンポーネント
230          * @param rect AWTコンポーネント内の座標系を基準とした長方形領域
231          */
232         public void start(Component source, Rectangle rect) {
233                 Point point = rect.getLocation();
234                 point.translate( rect.width/2, rect.height/2 );
235                 start(source, point);
236         }
237         private long prevStartedAt = System.nanoTime();
238         /**
239          * 指定された場所から図形の表示を開始します。
240          * @param source AWTコンポーネント
241          * @param point AWTコンポーネント内の座標系を基準とした場所
242          */
243         public void start(Component source, Point point) {
244                 long startedAt = System.nanoTime();
245                 if( startedAt - prevStartedAt < (INTERVAL_MS * 1000)*50 ) {
246                         // 頻繁すぎる場合は無視する
247                         return;
248                 }
249                 point = SwingUtilities.convertPoint(source, point, this);
250                 synchronized(queue) { queue.add(new QueueEntry(point)); }
251                 timer.start();
252                 prevStartedAt = startedAt;
253         }
254 }