GIFAnimation.java
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13: package xeij;
14:
15: import java.awt.*;
16: import java.awt.event.*;
17: import java.awt.image.*;
18: import java.io.*;
19: import java.util.*;
20: import javax.imageio.*;
21: import javax.imageio.metadata.*;
22: import javax.imageio.stream.*;
23: import javax.swing.*;
24: import javax.swing.event.*;
25: import org.w3c.dom.*;
26:
27: public class GIFAnimation {
28:
29:
30:
31:
32:
33:
34: public static final int GIF_WAITING_TIME_MIN = 0;
35: public static final int GIF_WAITING_TIME_MAX = 30;
36: public static final int GIF_RECORDING_TIME_MIN = 1;
37: public static final int GIF_RECORDING_TIME_MAX = 30;
38: public static final int GIF_MAGNIFICATION_MIN = 10;
39: public static final int GIF_MAGNIFICATION_MAX = 200;
40:
41: public static int gifWaitingTime;
42: public static int gifRecordingTime;
43: public static int gifMagnification;
44: public static Object gifInterpolation;
45:
46: public static SpinnerNumberModel gifWaitingTimeModel;
47: public static SpinnerNumberModel gifRecordingTimeModel;
48: public static SpinnerNumberModel gifMagnificationModel;
49:
50: public static JMenuItem gifStartRecordingMenuItem;
51: public static JMenu gifSettingsMenu;
52:
53: public static java.util.Timer gifTimer;
54:
55: public static int gifScreenWidth;
56: public static int gifScreenHeight;
57: public static int gifStretchWidth;
58: public static int gifStereoscopicFactor;
59: public static boolean gifStereoscopicOn;
60: public static int gifStereoscopicMethod;
61:
62: public static double gifDelayTime;
63: public static int gifWaitingFrames;
64: public static int gifRecordingFrames;
65: public static int[] gifBuffer;
66: public static int gifPointer;
67: public static int gifWaitingCounter;
68: public static int gifRecordingCounter;
69: public static boolean gifNowRecording;
70:
71:
72:
73: public static void gifInit () {
74:
75: gifWaitingTime = Math.max (GIF_WAITING_TIME_MIN, Math.min (GIF_WAITING_TIME_MAX, Settings.sgsGetInt ("gifwaitingtime")));
76: gifRecordingTime = Math.max (GIF_RECORDING_TIME_MIN, Math.min (GIF_RECORDING_TIME_MAX, Settings.sgsGetInt ("gifrecordingtime")));
77: gifMagnification = Math.max (GIF_MAGNIFICATION_MIN, Math.min (GIF_MAGNIFICATION_MAX, Settings.sgsGetInt ("gifmagnification")));
78: switch (Settings.sgsGetString ("gifinterpolation").toLowerCase ()) {
79: case "nearest":
80: gifInterpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
81: break;
82: case "bilinear":
83: gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
84: break;
85: case "bicubic":
86: gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
87: break;
88: default:
89: gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
90: }
91:
92: gifWaitingTimeModel = new SpinnerNumberModel (gifWaitingTime, GIF_WAITING_TIME_MIN, GIF_WAITING_TIME_MAX, 1);
93: gifRecordingTimeModel = new SpinnerNumberModel (gifRecordingTime, GIF_RECORDING_TIME_MIN, GIF_RECORDING_TIME_MAX, 1);
94: gifMagnificationModel = new SpinnerNumberModel (gifMagnification, GIF_MAGNIFICATION_MIN, GIF_MAGNIFICATION_MAX, 1);
95:
96: ButtonGroup interpolationGroup = new ButtonGroup ();
97:
98: gifStartRecordingMenuItem =
99: Multilingual.mlnText (
100: ComponentFactory.createMenuItem (
101: "Start recording",
102: new ActionListener () {
103: @Override public void actionPerformed (ActionEvent ae) {
104: gifStartRecording ();
105: }
106: }),
107: "ja", "録画開始");
108:
109: gifSettingsMenu =
110: Multilingual.mlnText (
111: ComponentFactory.createMenu (
112: "GIF animation recording settings",
113: ComponentFactory.createHorizontalBox (
114: Multilingual.mlnText (ComponentFactory.createLabel ("Waiting time"), "ja", "待ち時間"),
115: Box.createHorizontalGlue ()
116: ),
117: ComponentFactory.createHorizontalBox (
118: Box.createHorizontalStrut (20),
119: ComponentFactory.createNumberSpinner (gifWaitingTimeModel, 4, new ChangeListener () {
120: @Override public void stateChanged (ChangeEvent ce) {
121: gifWaitingTime = gifWaitingTimeModel.getNumber ().intValue ();
122: }
123: }),
124: Multilingual.mlnText (ComponentFactory.createLabel ("seconds"), "ja", "秒"),
125: Box.createHorizontalGlue ()
126: ),
127: ComponentFactory.createHorizontalBox (
128: Multilingual.mlnText (ComponentFactory.createLabel ("Recording time"), "ja", "録画時間"),
129: Box.createHorizontalGlue ()
130: ),
131: ComponentFactory.createHorizontalBox (
132: Box.createHorizontalStrut (20),
133: ComponentFactory.createNumberSpinner (gifRecordingTimeModel, 4, new ChangeListener () {
134: @Override public void stateChanged (ChangeEvent ce) {
135: gifRecordingTime = gifRecordingTimeModel.getNumber ().intValue ();
136: }
137: }),
138: Multilingual.mlnText (ComponentFactory.createLabel ("seconds"), "ja", "秒"),
139: Box.createHorizontalGlue ()
140: ),
141: ComponentFactory.createHorizontalBox (
142: Multilingual.mlnText (ComponentFactory.createLabel ("Magnification"), "ja", "倍率"),
143: Box.createHorizontalGlue ()
144: ),
145: ComponentFactory.createHorizontalBox (
146: Box.createHorizontalStrut (20),
147: ComponentFactory.createNumberSpinner (gifMagnificationModel, 4, new ChangeListener () {
148: @Override public void stateChanged (ChangeEvent ce) {
149: gifMagnification = gifMagnificationModel.getNumber ().intValue ();
150: }
151: }),
152: ComponentFactory.createLabel ("%"),
153: Box.createHorizontalGlue ()
154: ),
155: ComponentFactory.createHorizontalSeparator (),
156: Multilingual.mlnText (
157: ComponentFactory.createRadioButtonMenuItem (
158: interpolationGroup,
159: gifInterpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR,
160: "Nearest neighbor",
161: new ActionListener () {
162: @Override public void actionPerformed (ActionEvent ae) {
163: gifInterpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
164: }
165: }),
166: "ja", "最近傍補間"),
167: Multilingual.mlnText (
168: ComponentFactory.createRadioButtonMenuItem (
169: interpolationGroup,
170: gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BILINEAR,
171: "Bilinear",
172: new ActionListener () {
173: @Override public void actionPerformed (ActionEvent ae) {
174: gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
175: }
176: }),
177: "ja", "線形補間"),
178: Multilingual.mlnText (
179: ComponentFactory.createRadioButtonMenuItem (
180: interpolationGroup,
181: gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BICUBIC,
182: "Bicubic",
183: new ActionListener () {
184: @Override public void actionPerformed (ActionEvent ae) {
185: gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
186: }
187: }),
188: "ja", "三次補間")
189: ),
190: "ja", "GIF アニメーション録画設定");
191:
192: gifTimer = new java.util.Timer ();
193: }
194:
195:
196:
197: public static void gifTini () {
198: Settings.sgsPutInt ("gifwaitingtime", gifWaitingTime);
199: Settings.sgsPutInt ("gifrecordingtime", gifRecordingTime);
200: Settings.sgsPutInt ("gifmagnification", gifMagnification);
201: Settings.sgsPutString ("gifinterpolation",
202: gifInterpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR ? "nearest" :
203: gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BILINEAR ? "bilinear" :
204: gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BICUBIC ? "bicubic" :
205: "bilinear");
206: }
207:
208:
209:
210: public static void gifStartRecording () {
211: if (gifNowRecording) {
212: return;
213: }
214:
215: gifScreenWidth = XEiJ.pnlScreenWidth;
216: gifScreenHeight = XEiJ.pnlScreenHeight;
217: gifStretchWidth = XEiJ.pnlStretchWidth;
218: gifStereoscopicFactor = XEiJ.pnlStereoscopicFactor;
219: gifStereoscopicOn = XEiJ.pnlStereoscopicOn;
220: gifStereoscopicMethod = XEiJ.pnlStereoscopicMethod;
221:
222:
223:
224:
225:
226:
227:
228:
229:
230:
231:
232:
233:
234:
235: gifDelayTime = Math.max (2.0, (double) CRTC.crtTotalLength * 1e-1 + (double) CRTC.crtTotalLengthMNP * 1e-10);
236:
237:
238:
239:
240:
241: gifWaitingFrames = (int) Math.floor ((double) gifWaitingTime * 100.0 / gifDelayTime + 0.5);
242: gifRecordingFrames = (int) Math.floor ((double) gifRecordingTime * 100.0 / gifDelayTime + 0.5);
243:
244: int fullSize = gifScreenWidth * gifScreenHeight * gifStereoscopicFactor;
245: int maxNumberOfFrames = 0x7fffffff / fullSize;
246: if (maxNumberOfFrames < gifRecordingFrames) {
247: return;
248: }
249:
250: int bufferSize = fullSize * gifRecordingFrames;
251: try {
252: gifBuffer = new int[bufferSize];
253: } catch (OutOfMemoryError oome) {
254: oome.printStackTrace ();
255: return;
256: }
257:
258: gifStartRecordingMenuItem.setEnabled (false);
259: gifPointer = 0;
260: gifWaitingCounter = 0;
261: gifRecordingCounter = 0;
262: gifNowRecording = true;
263: CRTC.crtCaptureClock = XEiJ.mpuClockTime;
264: CRTC.crtFrameTaskClock = Math.min (CRTC.crtContrastClock, CRTC.crtCaptureClock);
265: }
266:
267:
268:
269: public static void gifCaptureFrame () {
270: if (gifWaitingCounter < gifWaitingFrames) {
271: gifWaitingCounter++;
272: return;
273: }
274:
275: if (XEiJ.PNL_USE_THREAD) {
276: int[] bitmap = XEiJ.pnlBMLeftArray[XEiJ.pnlBMWrite & 3];
277: for (int y = 0; y < gifScreenHeight; y++) {
278: System.arraycopy (bitmap, XEiJ.PNL_BM_WIDTH * y,
279: gifBuffer, gifPointer,
280: gifScreenWidth);
281: gifPointer += gifScreenWidth;
282: }
283: if (gifStereoscopicFactor == 2) {
284: bitmap = XEiJ.pnlBMRightArray[XEiJ.pnlBMWrite & 3];
285: for (int y = 0; y < gifScreenHeight; y++) {
286: System.arraycopy (bitmap, XEiJ.PNL_BM_WIDTH * y,
287: gifBuffer, gifPointer,
288: gifScreenWidth);
289: gifPointer += gifScreenWidth;
290: }
291: }
292: } else {
293: for (int y = 0; y < gifScreenHeight; y++) {
294: System.arraycopy (XEiJ.pnlBMLeft, XEiJ.PNL_BM_WIDTH * y,
295: gifBuffer, gifPointer,
296: gifScreenWidth);
297: gifPointer += gifScreenWidth;
298: }
299: if (gifStereoscopicFactor == 2) {
300: for (int y = 0; y < gifScreenHeight; y++) {
301: System.arraycopy (XEiJ.pnlBMRight, XEiJ.PNL_BM_WIDTH * y,
302: gifBuffer, gifPointer,
303: gifScreenWidth);
304: gifPointer += gifScreenWidth;
305: }
306: }
307: }
308: gifRecordingCounter++;
309: if (gifRecordingCounter == gifRecordingFrames) {
310:
311: CRTC.crtCaptureClock = XEiJ.FAR_FUTURE;
312: CRTC.crtFrameTaskClock = Math.min (CRTC.crtContrastClock, CRTC.crtCaptureClock);
313:
314: gifTimer.schedule (new TimerTask () {
315: @Override public void run () {
316: gifOutput ();
317: }
318: }, 0L);
319: }
320: }
321:
322:
323:
324: public static void gifOutput () {
325: System.out.println (Multilingual.mlnJapanese ? "画像を圧縮しています" : "Compressing images");
326:
327: double zoomRatio = (double) gifMagnification / 100.0;
328: int zoomWidth = (int) Math.floor ((double) gifStretchWidth * zoomRatio + 0.5);
329: int zoomHeight = (int) Math.floor ((double) gifScreenHeight * zoomRatio + 0.5);
330:
331: BufferedImage imageLeft = new BufferedImage (XEiJ.PNL_BM_WIDTH, gifScreenHeight, BufferedImage.TYPE_INT_RGB);
332: BufferedImage imageRight = new BufferedImage (XEiJ.PNL_BM_WIDTH, gifScreenHeight, BufferedImage.TYPE_INT_RGB);
333: int[] bmLeft = ((DataBufferInt) imageLeft.getRaster ().getDataBuffer ()).getData ();
334: int[] bmRight = ((DataBufferInt) imageRight.getRaster ().getDataBuffer ()).getData ();
335:
336: BufferedImage image = new BufferedImage (zoomWidth * gifStereoscopicFactor, zoomHeight, BufferedImage.TYPE_INT_RGB);
337: Graphics2D g2 = image.createGraphics ();
338: g2.setRenderingHint (RenderingHints.KEY_INTERPOLATION, gifInterpolation);
339:
340: ImageWriter imageWriter = ImageIO.getImageWritersBySuffix ("gif").next ();
341: ImageWriteParam writeParam = imageWriter.getDefaultWriteParam ();
342: try {
343:
344: String dirName = "capture";
345: File dir = new File (dirName);
346: if (dir.exists () ? !dir.isDirectory () : !dir.mkdir ()) {
347: gifBuffer = null;
348: gifNowRecording = false;
349: gifStartRecordingMenuItem.setEnabled (true);
350: return;
351: }
352: HashSet<String> nameSet = new HashSet<String> ();
353: for (String name : dir.list ()) {
354: nameSet.add (name);
355: }
356: int number = 0;
357: String name;
358: do {
359: number++;
360: name = number + ".gif";
361: } while (!nameSet.add (name));
362: name = dirName + "/" + name;
363:
364: File file = new File (name);
365: if (file.exists ()) {
366: file.delete ();
367: }
368: ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream (file);
369: imageWriter.setOutput (imageOutputStream);
370: imageWriter.prepareWriteSequence (null);
371:
372: int halfSize = gifScreenWidth * gifScreenHeight;
373: int fullSize = halfSize * gifStereoscopicFactor;
374:
375: double lastTime = 0.0;
376: double lastIntTime = 0.0;
377:
378: for (int counter = 0; counter < gifRecordingFrames; ) {
379: int pointer = fullSize * counter;
380:
381: int span = 1;
382: while (counter + span < gifRecordingFrames &&
383: Arrays.equals (gifBuffer, pointer, pointer + fullSize,
384: gifBuffer, pointer + fullSize * span, pointer + fullSize * (span + 1))) {
385: span++;
386: }
387: counter += span;
388:
389: double time = lastTime + gifDelayTime * (double) span;
390: double intTime = Math.floor (time + 0.5);
391:
392: IIOMetadata metadata = makeMetadata (imageWriter,
393: writeParam,
394: image,
395:
396:
397:
398: String.valueOf ((int) (intTime - lastIntTime)));
399: lastTime = time;
400: lastIntTime = intTime;
401:
402:
403: for (int y = 0; y < gifScreenHeight; y++) {
404: System.arraycopy (gifBuffer, pointer,
405: bmLeft, XEiJ.PNL_BM_WIDTH * y,
406: gifScreenWidth);
407: pointer += gifScreenWidth;
408: }
409: if (gifStereoscopicFactor == 2) {
410: for (int y = 0; y < gifScreenHeight; y++) {
411: System.arraycopy (gifBuffer, pointer,
412: bmRight, XEiJ.PNL_BM_WIDTH * y,
413: gifScreenWidth);
414: pointer += gifScreenWidth;
415: }
416: }
417:
418: g2.setColor (Color.black);
419: g2.fillRect (0, 0, zoomWidth * gifStereoscopicFactor, zoomHeight);
420: if (XEiJ.PNL_STEREOSCOPIC_ON && gifStereoscopicOn) {
421: if (gifStereoscopicMethod == XEiJ.PNL_NAKED_EYE_CROSSING) {
422: g2.drawImage (imageRight,
423: 0, 0, zoomWidth, zoomHeight,
424: 0, 0, gifScreenWidth, gifScreenHeight,
425: null);
426: g2.drawImage (imageLeft,
427: zoomWidth, 0, zoomWidth * 2, zoomHeight,
428: 0, 0, gifScreenWidth, gifScreenHeight,
429: null);
430: } else if (gifStereoscopicMethod == XEiJ.PNL_NAKED_EYE_PARALLEL ||
431: gifStereoscopicMethod == XEiJ.PNL_SIDE_BY_SIDE) {
432: g2.drawImage (imageLeft,
433: 0, 0, zoomWidth, zoomHeight,
434: 0, 0, gifScreenWidth, gifScreenHeight,
435: null);
436: g2.drawImage (imageRight,
437: zoomWidth, 0, zoomWidth * 2, zoomHeight,
438: 0, 0, gifScreenWidth, gifScreenHeight,
439: null);
440: } else {
441: g2.drawImage (imageLeft,
442: 0, 0, zoomWidth, zoomHeight >> 1,
443: 0, 0, gifScreenWidth, gifScreenHeight,
444: null);
445: g2.drawImage (imageRight,
446: 0, zoomHeight >> 1, zoomWidth, zoomHeight,
447: 0, 0, gifScreenWidth, gifScreenHeight,
448: null);
449: }
450: } else {
451: g2.drawImage (imageLeft,
452: 0, 0, zoomWidth, zoomHeight,
453: 0, 0, gifScreenWidth, gifScreenHeight,
454: null);
455: }
456:
457: imageWriter.writeToSequence (new IIOImage (image, null, metadata), writeParam);
458: }
459:
460: imageWriter.endWriteSequence ();
461: imageOutputStream.close ();
462: System.out.println (Multilingual.mlnJapanese ? name + " を更新しました" : name + " was updated");
463: } catch (IOException ioe) {
464: ioe.printStackTrace ();
465: }
466: gifBuffer = null;
467: gifNowRecording = false;
468: gifStartRecordingMenuItem.setEnabled (true);
469: }
470:
471: public static IIOMetadata makeMetadata (ImageWriter imageWriter, ImageWriteParam writeParam, BufferedImage image, String delayTime) {
472: IIOMetadata metadata = imageWriter.getDefaultImageMetadata (new ImageTypeSpecifier (image), writeParam);
473: String metaFormat = metadata.getNativeMetadataFormatName ();
474: Node root = metadata.getAsTree (metaFormat);
475: IIOMetadataNode gce = new IIOMetadataNode ("GraphicControlExtension");
476: gce.setAttribute ("delayTime", delayTime);
477: gce.setAttribute ("disposalMethod", "none");
478: gce.setAttribute ("transparentColorFlag", "FALSE");
479: gce.setAttribute ("transparentColorIndex", "0");
480: gce.setAttribute ("userInputFlag", "FALSE");
481: root.appendChild (gce);
482: try {
483: metadata.setFromTree (metaFormat, root);
484: } catch (IIOInvalidTreeException ite) {
485: ite.printStackTrace ();
486: }
487: return metadata;
488: }
489:
490: }