JavaでJPEGのExif情報を読む

Exif情報について

デジカメやスマホで撮影した画像は通常JPEGというファイルフォーマットで保存されますが、これにはExif(Exchangeable image file format)という付属情報が付与されていて、そこに画像のサイズやら撮影日時やら撮影したカメラのメーカーやら、場合によっては位置情報なんかも保存されていたりします。SNSなどに画像を上げるときには注意してください。
で、たいていのデジカメでは、画像の方向(カメラを縦にして撮影した縦長の写真か、横にして撮影した横長の写真か)といった情報もExifに保持しています。つまり、カメラを縦にして縦長の写真を撮っても、画像データそのものは横長画像と同じように保持されていて、Exif情報により表示時にひっくりかえす、ということが期待されているわけです。時々、そのあたりの扱いがずれていて、画像がひっくり返って表示されることがあります。
Javaでは、標準のライブラリでは、Exif情報をもとに画像を正しい向きにする、という機能はないようなので、ファイルフォーマットの勉強も兼ねて、自力でExifフォーマットを読み込んでみました。

Exifフォーマットについて

Exifのフォーマットについては、主に以下のページを参照しました。
www2.airnet.ne.jp
beyondjapan.com
以下の仕様書の英語をちまちま拾い読みしていたら、
https://www.exif.org/Exif2-2.PDF
日本語版があるでやんの。もともと日本の企画ですからね……
http://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf

ソースにコメントを過剰に入れておいたので、それを見ればフォーマットもわかるかと思います。

作ってみて

  • Javaのbyte型が符号付き(しかない)っての、端的に頭おかしい。
  • カメラによるのでしょうが、大昔、初代AndroidスマホHT-03Aで撮った縦長写真にはExifに画像方向がついてなくて、画像そのものが縦向きになっていた。最初これに気づかず、画像方向どこだ…… と悩んでいた。
  • むやみにいっぱい出ているUserCommentの項目(タグNo.0x9286)、コメントなんて特に何も入れたおぼえはないし、文字コードはASCIIになっているが文字列として復元できないんだけど、なんだろこれ。個人情報とか含んでないよね……

ソース

package com.kmaebashi.exifreadertest;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.nio.ByteOrder;

public class ExifReader {
    public static void main(String[] args) {
        String filePath = "JPEGファイルのパスをここに書いてください";

        try (DataInputStream inStream = new DataInputStream(
                new BufferedInputStream(
                        new FileInputStream(filePath)))) {
            // 冒頭12バイトの構成
            // 先頭2バイト: 0xff 0xd8 JPEGファイルはこれで固定
            // 次の2バイト: 0xff 0xe1 APP1(Application Marker Segment 1)のマーカー
            // 次の4バイト: APP1領域のサイズ(ビッグエンディアン)
            // 次の6バイト: Exifのマーカー('E', 'x', 'i', 'f', 00, 00)
            byte[] headersByte = new byte[12];
            int[] headers;
            int app1Size;
            if (inStream.read(headersByte) != headersByte.length) {
                System.err.println("ファイルが小さすぎます。");
                System.exit(1);
            }
            headers = unsignedByteArrayToIntArray(headersByte);
            if (headers[0] != 0xff || headers[1] != 0xd8) {
                System.err.println("JPEGファイルではありません。");
                System.exit(1);
            }
            if (headers[2] != 0xff || headers[3] != 0xe1) {
                System.err.println("APP1データが含まれません。");
                System.exit(1);
            }
            app1Size = read2Byte(headers, 4, ByteOrder.BIG_ENDIAN);
            if (headers[6] != (byte)'E'
                || headers[7] != (byte)'x'
                || headers[8] != (byte)'i'
                || headers[9] != (byte)'f'
                || headers[10] != 0x00 || headers[11] != 0x00) {
                System.err.println("Exifデータが含まれません。");
                System.exit(1);
            }
            byte[] app1DataByte = new byte[app1Size];
            if (inStream.read(app1DataByte) < app1Size) {
                System.err.println("ヘッダ情報に対し、ファイルサイズが小さすぎます。");
                System.exit(1);
            }

            // Javaのbyteは符号付きでまともにバイトを扱えないので、intの配列に変換する。
            int[] app1Data = unsignedByteArrayToIntArray(app1DataByte);

            // APP1の冒頭8バイトはTIFFヘッダ
            // 先頭2バイト: バイトオーダーを示す。
            //       0x49, 0x49('I', 'I')...リトルエンディアン(Intelの略らしい)
            //       0x4d, 0x4d('M', 'M')...ビッグエンディアン(Motorolaの略らしい)
            // 次の2バイト: 0x00, 0x2a TIFF識別コード(固定)
            // 次の4バイト:
            ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN; // make compiler happy
            if (app1Data[0] == 0x49 && app1Data[1] == 0x49) {
                byteOrder = ByteOrder.LITTLE_ENDIAN;
            } else if (app1Data[0] == 0x4d && app1Data[1] == 0x4d) {
                byteOrder = ByteOrder.BIG_ENDIAN;
            } else {
                System.err.println("バイトオーダーが不正です。");
                System.exit(1);
            }
            if (read2Byte(app1Data, 2, byteOrder) != 0x002a) {
                System.err.println("TIFF識別子が002aではありません。");
                System.exit(1);
            }

            // 最初のIFD(Image File Directory)である0th IFDのオフセットを取得。
            // これを含め、以後出てくるオフセットは、すべてAPP1の先頭を起点とする。
            // ここまで、0th IFDのオフセットを含めて8バイト使っているので、
            // その続きとなる0th IFDの先頭のオフセットはたいてい8。
            int offsetOf0thIFD = read4Byte(app1Data, 4, byteOrder);
            System.out.println("0thIFDのオフセット(たいてい8)…" + offsetOf0thIFD);

            // 0th IFDのタグの数を取得(先頭2バイト)
            int numOf0thIFDTags = read2Byte(app1Data, offsetOf0thIFD, byteOrder);
            System.out.println("0thIFDのタグの数…" + numOf0thIFDTags);

            System.out.println("**** 0th IFD ****");
            // 各タグについて内容出力。各タグは12バイトの固定長。
            for (int i = 0; i < numOf0thIFDTags; i++) {
                // 8はTIFFヘッダ、2はタグの数の分
                dumpIFDTag(app1Data, 8 + 2 + i * 12, byteOrder);
            }

            // Exif IFDがあれば、それも出力する。
            if (exifOffset >= 0) {
                int numOfExifIFDTags = read2Byte(app1Data, exifOffset, byteOrder);
                System.out.println("**** EXIF IFD ****");
                System.out.println("Exif IFDのタグの数…" + numOfExifIFDTags);
                for (int i = 0; i < numOfExifIFDTags; i++) {
                    dumpIFDTag(app1Data, exifOffset + 2 + i * 12, byteOrder);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
            System.exit(2);
        }
    }

    private static class TagType {
        public String name;
        public int size;
        public TagType(String name, int size) {
            this.name = name;
            this.size = size;
        }
    }

    // IDFのタグの型情報を保持する配列。
    // Exifの仕様書(JEITA CP-3451) https://www.exif.org/Exif2-2.PDF を参照。
    private static TagType[] tagTypeData = {
        null,
        new TagType("BYTE", 1),
        new TagType("ASCII", 1), // 末尾には'\0'が入る
        new TagType("SHORT", 2),
        new TagType("LONG", 4),
        new TagType("RATIONAL", 8), // 分数
        null,
        new TagType("UNDEFINED", 1),
        null,
        new TagType("SLONG", 4), // 符号付
        new TagType("SRATIONAL", 8),
    };

    private static int exifOffset = -1;

    private static void dumpIFDTag(int[] array, int offset, ByteOrder byteOrder) {
        // ひとつのIFDタグ(固定長12バイト)の構成は以下の通り。
        // 先頭2バイト: タグNo。定義はExifの仕様書(JEITA CP-3451)を参照。
        // 次の2バイト: そのタグの型。
        // 次の4バイト: そのタグに含まれる値の数。
        // 次の4バイト: 値またはオフセット。示すべき値が4バイトに収まる場合はここに
        //              格納され、収まらない場合は、値の場所を示すオフセットが格納される。
        int tagNo = read2Byte(array, offset, byteOrder);
        int tagType = read2Byte(array, offset + 2, byteOrder);
        int numOfValues = read4Byte(array, offset + 4, byteOrder);

        System.out.print(String.format("%04x", tagNo) + ":"
                           + tagTypeData[tagType].name + ":"
                           + numOfValues + ":");

        if (tagTypeData[tagType].size * numOfValues <= 4) {
            printTagValue(array, offset + 8, tagType, numOfValues, byteOrder);
            System.out.println("");
        } else {
            int valueOffset = read4Byte(array, offset + 8, byteOrder);
            printTagValue(array, valueOffset, tagType, numOfValues, byteOrder);
            System.out.println("");
        }

        // 0th IFDの中にExif IFDのオフセット(タグNo.0x8769)があったら
        // static変数exitOffsetに退避する。
        if (tagNo == 0x8769) {
            exifOffset = read4Byte(array, offset + 8, byteOrder);
        }
    }

    private static void printTagValue(int[] array, int offset, int tagType, int numOfValues, ByteOrder byteOrder) {
        for (int i = 0; i < numOfValues; i++) {
            if (i > 0) {
                System.out.print(", ");
            }
            if (tagTypeData[tagType].name == "ASCII") {
                if (array[offset + i] == 0) {
                    System.out.print("'\\0'");
                } else {
                    System.out.print("" + (char)array[offset + i]);
                }
            } else if (tagTypeData[tagType].size == 1) {
                int value = array[offset + i];
                System.out.print(String.format("%02x", value));
            } else if (tagTypeData[tagType].size == 2) {
                int value = read2Byte(array, offset + (i * 2), byteOrder);
                System.out.print(String.format("%04x", value));
            } else if (tagTypeData[tagType].size == 4) {
                int value = read4Byte(array, offset + (i * 4), byteOrder);
                System.out.print(String.format("%08x", value));
            } else if (tagTypeData[tagType].size == 8) {
                int numerator = read4Byte(array, offset + (i * 8), byteOrder);
                int denominator = read4Byte(array, offset + (i * 8) + 4, byteOrder);
                System.out.print("" + numerator + "/" + denominator);
            }
        }
    }

    private static int[] unsignedByteArrayToIntArray(byte[] src) {
        int[] dest = new int[src.length];

        for (int i = 0; i < src.length; i++) {
            dest[i] = Byte.toUnsignedInt(src[i]);
        }

        return dest;
    }

    public static int read2Byte(int[] array, int offset, ByteOrder byteOrder) {
        int ret;
        if (byteOrder == ByteOrder.BIG_ENDIAN) {
            ret = array[offset] * 256 + array[offset + 1];
        } else {
            ret = array[offset + 1] * 256 + array[offset];
        }
        return ret;
    }

    public static int read4Byte(int[] array, int offset, ByteOrder byteOrder) {
        int ret;
        if (byteOrder == ByteOrder.BIG_ENDIAN) {
            ret = array[offset] * (256 * 256 * 256) + array[offset + 1] * 65536
                    + array[offset + 2] * 256 + array[offset + 3];
        } else {
            ret = array[offset + 3] * (256 * 256 * 256) + array[offset + 2] * 65536
                    + array[offset + 1] * 256 + array[offset];
        }
        return ret;
    }
}

出力例

Galaxy Note 8で撮った縦長写真です。先日しまなみ海道に行った時のやつ。

0thIFDのオフセット(たいてい8)…8
0thIFDのタグの数…13
**** 0th IFD ****
0100:LONG:1:00000fc0
0101:LONG:1:00000bd0
010f:ASCII:8:s, a, m, s, u, n, g, '\0'
0110:ASCII:7:S, C, -, 0, 1, K, '\0'
0112:SHORT:1:0006 ←これが画像の向きを示す。6は、時計回りに90°回せば元に戻る向き。
011a:RATIONAL:1:72/1
011b:RATIONAL:1:72/1
0128:SHORT:1:0002
0131:ASCII:14:S, C, 0, 1, K, O, M, U, 1, C, S, G, 3, '\0'
0132:ASCII:20:2, 0, 1, 9, :, 0, 9, :, 1, 4,  , 1, 8, :, 0, 0, :, 5, 4, '\0'
0213:SHORT:1:0001
8769:LONG:1:000000ec
8825:LONG:1:0000175a
**** EXIF IFD ****
Exif IFDのタグの数…31
829a:RATIONAL:1:1/40
829d:RATIONAL:1:170/100
8822:SHORT:1:0002
8827:SHORT:1:00c8
9000:UNDEFINED:4:30, 32, 32, 30
9003:ASCII:20:2, 0, 1, 9, :, 0, 9, :, 1, 4,  , 1, 8, :, 0, 0, :, 5, 4, '\0'
9004:ASCII:20:2, 0, 1, 9, :, 0, 9, :, 1, 4,  , 1, 8, :, 0, 0, :, 5, 4, '\0'
9101:UNDEFINED:4:01, 02, 03, 00
9201:SRATIONAL:1:5321/1000
9202:RATIONAL:1:153/100
9203:SRATIONAL:1:93/100
9204:SRATIONAL:1:0/10
9205:RATIONAL:1:153/100
9207:SHORT:1:0002
9208:SHORT:1:0000
9209:SHORT:1:0000
920a:RATIONAL:1:430/100
927c:UNDEFINED:98:07, 00, 01, 00, 07, 00, 04, 00, 00, 00, 30, 31, 30, 30, 02, 00, 04, 00, 01, 00, 00, 00, 00, 20, 01, 00, 0c, 00, 04, 00, 01, 00, 00, 00, 00, 00, 00, 00, 10, 00, 05, 00, 01, 00, 00, 00, 5a, 00, 00, 00, 40, 00, 04, 00, 01, 00, 00, 00, 00, 00, 00, 00, 50, 00, 04, 00, 01, 00, 00, 00, 01, 00, 00, 00, 00, 01, 03, 00, 01, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00
9286:UNDEFINED:5107:41, 53, 43, 49, 49, 00, 00, 00, 0a, 00, 00, 00, <なんかいっぱい出てるので中略> 30, 20, 00
a000:UNDEFINED:4:30, 31, 30, 30
a001:SHORT:1:0001
a002:LONG:1:00000fc0
a003:LONG:1:00000bd0
a005:LONG:1:0000173c
a217:SHORT:1:0002
a301:UNDEFINED:1:01
a402:SHORT:1:0000
a403:SHORT:1:0000
a405:SHORT:1:001a
a406:SHORT:1:0000
a420:ASCII:24:G, 1, 2, Q, S, K, A, 0, 2, S, M,  , G, 1, 2, Q, S, K, D, 0, 1, S, A, '\0'

しまなみ海道にリベンジに行きました。今度こそ。(往復編)

昨年の7月、自転車乗りの聖地とも楽園とも言われるしまなみ海道にはじめて行き、当日は素晴らしく天気がよかったものの1週間前の洪水の影響でグルメライドとかがあまりできず、今年の7月リベンジに行ったら今度は当日の天気が悪かった、ということで、9/14~9/15にかけて再度リベンジに行きました。

kmaebashi.hatenablog.com

kmaebashi.hatenablog.com

kmaebashi.hatenablog.com今回は、天気を見極めてから日程を決めたわけですが、そんな直前では今治の宿は取れません。過去2回は、始発の新幹線で尾道まで行って1日かけて今治周辺まで走り、1泊して、帰りはまた1日かけて尾道まで戻ってくる、という計画でしたが(前回は雨のため帰りは輪行になりましたが)、今回はまず尾道で1泊して、早起きして早朝から走り始め、1日で今治までを往復する、というコースになりました。尾道駅近くのホテルから走り始めて、新幹線に乗れる新尾道駅まで戻って約146km、名古屋から京都までの150kmとだいたい同距離です。

しまなみ海道はもうちょっとのんびり回るべきコースのような気もしますが、たいへん天気が良かったので十分に楽しめました。

さて、出発。土曜の昼過ぎ、名古屋からのぞみで出て岡山でこだまに乗り換えます。

f:id:kmaebashi:20190914142832j:plain

新尾道駅輪行解除。

f:id:kmaebashi:20190914151352j:plain

15時過ぎには着いたので、ちょっとは尾道も散策できます。尾道ラーメンとやらを食べてみようと、どうやら元祖らしい朱華園に行ってみたら…… orz

f:id:kmaebashi:20190914154309j:plain

すきっ腹を抱えて、「尾道映画資料館」を観に来ました。

f:id:kmaebashi:20190914163338j:plain

f:id:kmaebashi:20190914163351j:plain

尾道の映画といえば転校生だろう大林宣彦だろう、と思ったら、小津安二郎推しであった。東京物語すら観てないよ……

おのみち歴史博物館にも行ってみました。正直、狭くて収蔵品も少ないわ、その収蔵品も仏像とかで、まあ。

f:id:kmaebashi:20190914163205j:plain

中でやってた「尾道マンガ大賞展」の方が見ごたえありました。地元の小中学生のマンガですが。

 

f:id:kmaebashi:20190914163217j:plain

私にとってはやっぱり尾道といえば転校生なので、一夫と一美が転がり落ちた石段、御袖天満宮を見に行きました。

f:id:kmaebashi:20190914174025j:plain

これが石段。いやかなり急だよ怖いよ。

f:id:kmaebashi:20190914174031j:plain

角度がわかるように。

f:id:kmaebashi:20190914174339j:plain

石段のこの石に、継ぎ目がどこにもない。1本で切り出したらしい。元の石をどうやって運んでどうやって切り出してどうやって据え付けたのか。

別の店で尾道ラーメンも食べました。

f:id:kmaebashi:20190914175748j:plain

尾道ラーメンって平麺かと思ってましたが、ここのは違ったな。

f:id:kmaebashi:20190914180611j:plain

その後、ホテルの1Fの飲み屋さんで飲む。

f:id:kmaebashi:20190914192203j:plain

f:id:kmaebashi:20190914193517j:plain

でまあ酔っぱらって早々に寝てしまって、翌朝、出発です。

同じようにしまなみ海道を往復しようとする人の参考のため、ここからは各写真に、写真のタイムスタンプを付けていきます。

06:04 ホテルを出発。本当はもうちょっと早く出たかったのですが、微妙に準備に時間が取られた。

f:id:kmaebashi:20190915060447j:plain

06:10 尾道から最初の島である向島まではフェリーで渡ります。フェリー乗り場のアスファルトにあったイラストがこれ。

f:id:kmaebashi:20190915061040j:plain

06:11 この、尾道前6:06の始便に乗りたかったのですが、1本遅れた。まあ、12分間隔なので大した問題ではないですが。

f:id:kmaebashi:20190915061146j:plain

なお、尾道から向島まで渡るフェリーはいくつかありますが、私が使ったのはここです(写真は7月のやつ)。

f:id:kmaebashi:20190713093653j:plain

06:14 フェリーの桟橋から。もう9月なので、日の出も遅くなっていますね。

f:id:kmaebashi:20190915061407j:plain

06:16 フェリー来た。

f:id:kmaebashi:20190915061652j:plain

06:40 向島から因島に渡る、因島大橋が見えてきました(ここから、橋の入り口までが遠いんだけどさ)。

f:id:kmaebashi:20190915064033j:plain

07:27 因島から生口島へ渡る生口橋

f:id:kmaebashi:20190915072738j:plain

 生口橋から見た海。どうもしまなみ海道に来ると橋の写真ばかりになってしまいますが。

f:id:kmaebashi:20190915072735j:plain

07:30 生口橋から降りるところ。橋は高いところにかかっているので、橋を渡るたびに、こんな道でくねくねと登ったり下りたりします。

f:id:kmaebashi:20190915073046j:plain

08:07 次の橋。多々羅大橋

f:id:kmaebashi:20190915080720j:plain

多々羅大橋から見た海。

f:id:kmaebashi:20190915080726j:plain

08:22 多々羅大橋を渡ったところに、多々羅しまなみ公園の道の駅というのがあり、そこに「サイクリストの聖地碑」があるのですが、店はまだ開いてない。自販機で水だけ買って進みます。

f:id:kmaebashi:20190915082235j:plain

f:id:kmaebashi:20190915082244j:plain

f:id:kmaebashi:20190915082337j:plain

 最近はどこに行ってもゆるキャラはあるものですが、大三島分校って、学校のゆるキャラなの?

f:id:kmaebashi:20190915082647j:plain

08:46 大三島橋伯方島に渡ります。

f:id:kmaebashi:20190915084816j:plain

08:58 伯方島にあった「しまなみ造船」の造船所。でかい。

f:id:kmaebashi:20190915085843j:plain

09:10 伯方島の道の駅まで行って、腹も減ったしからあげでも食べようかと思ったら、その手の温かい食べ物は10:00からだそうで、仕方がないのでソフトクリームでカロリー補給。

f:id:kmaebashi:20190915091028j:plain

09:31 伯方・大島大橋で大島に渡ります。なんか曇ってきたな。

f:id:kmaebashi:20190915093118j:plain

f:id:kmaebashi:20190915093124j:plain

09:39 大島に着きました。

f:id:kmaebashi:20190915093904j:plain

10:15 あんまり腹が減ったので、大島の道の駅で海鮮丼。

f:id:kmaebashi:20190915101534j:plain

10:56 最後で最長の橋、来島海峡大橋。でかい(この写真は、橋の中ほどで撮影)。
こういうのを見ると、「人類の科学は、技術は、こんな巨大なものを作ることができるというのか…」と言いたくなりますね。

f:id:kmaebashi:20190915105615j:plain

来島海峡大橋からの海。

f:id:kmaebashi:20190915105612j:plain

11:17 愛媛まで渡って、今回は「サンライズ糸山」(レンタサイクル施設)で折り返します。今治駅まではまだ5kmくらいありますが、一応ここがしまなみ海道の終端らしい(?)。

f:id:kmaebashi:20190915111740j:plain

実のところ来島海峡大橋を渡ればサンライズ糸山はすぐなのですが、ちょっと時間がかかっているのは、まさかこんなにすぐとは思わず行き過ぎてしまったからです……

11:26 サンライズ糸山から見える来島海峡大橋
ここにはレストランもあって、本来ここで昼食というのも考えていたのですが、腹が減りすぎて大島で食べてしまった。が、レストラン混みすぎだったのでそれで正解だったかも。

f:id:kmaebashi:20190915112657j:plain

12:21 どこだか忘れましたが、天気が良かったので撮った写真。いや本当に今回は天気に恵まれました。

f:id:kmaebashi:20190915122139j:plain

13:15 こういう木漏れ日とか好きなんですよー。

f:id:kmaebashi:20190915131529j:plain

13:40 なぜか伯方島の道の駅を見逃して、やばい水がないと思いながら多々羅しまなみ公園(サイクリストの聖地碑があるところ)まで来てしまいました。喉はからからだし、愛媛なのでまずポンジュース

f:id:kmaebashi:20190915134056j:plain

13:46 多々羅しまなみ公園道の駅から見た海と多々羅大橋

f:id:kmaebashi:20190915134620j:plain

去年来た時と同じですが、でこたんソフトも食べる。

f:id:kmaebashi:20190915134907j:plain

14:56 7月に来た時に寄ったジェラードの店、また寄ろうと思ったら大行列。

f:id:kmaebashi:20190915145625j:plain

15:02 で、あきらめて同じく7月に寄った隣のお店で地ダコ天と生絞りジュースをいただきます

f:id:kmaebashi:20190915150208j:plain

15:56 因島のローソンで最後の補給。

f:id:kmaebashi:20190915155654j:plain

16:18 因島の謎の恐竜

f:id:kmaebashi:20190915161844j:plain

16:25 因島大橋を渡ります。

f:id:kmaebashi:20190915162533j:plain

16:39 渡り終えた因島大橋。天気が怪しくなってきている。

f:id:kmaebashi:20190915163935j:plain

18:15 新尾道まで行って、帰りの新幹線の席を取って、輪行準備をしているところ。この辺で雨が降り出した。ぎりぎり助かりました。

f:id:kmaebashi:20190915181524j:plain

帰りののぞみで、ワゴン車が全然こなくて、ようやく来たかと思ったら弁当売り切れだった。

f:id:kmaebashi:20190915202931j:plain

総走行距離146.39km。

帰ってきてみると、しまなみ海道に画鋲が撒かれたなんてニュースもありましたが……

私自身がロードバイク乗りであるためバイアスはあるかもしれませんが、ことしまなみ海道で地元の人に恨まれるような無茶をしている自転車はそうそういないと思うんだけどなあ。

今後とも、しまなみ海道が、自転車乗りの楽園であってほしいものです。

 

 

ビワイチ(自転車による琵琶湖1周)に挑戦しました

例年、5月の連休と8月の夏休みには京都まで自転車で行っていたのですが、もう8回行ったのでさすがに飽きてきたし、ということで、この夏休みは琵琶湖一周(ビワイチ)に挑戦しました。名古屋から京都までなら150km程度、私にとっての1日の最長走行距離は5月のAACRの160kmだったのですが、琵琶湖1周だと200kmくらいあります(公称193km)。私にとっては初の200kmライドです。

琵琶湖1周なんて各自勝手に回ればよさそうなものですが、「ビワイチ」でぐぐると「輪の国びわ湖推進協議会」とかいう団体のページがトップに出ます。

www.biwako1.jp

おおむね、このサイトの地図にのっとって走ることにします。

f:id:kmaebashi:20190818224659p:plain

といっても、この地図を見ても、ルートは一通りではありません。細かい差異は置いておくとして、琵琶湖の最狭部にかかっている琵琶湖大橋を渡って、琵琶湖大橋より北の部分だけを1周するルートもあり、これをもってビワイチ達成、とする人もいるようですが、今回は愚直に全部回ることとします。

さて出発。なにしろ初の200kmライドなので、体力温存のため、琵琶湖湖畔まで行くのも自走ではなく輪行で。

宿は彦根に取ったので、新幹線で米原まで行って、米原駅輪行解除、自走で彦根まで行きます。

f:id:kmaebashi:20190811185120j:plain

彦根で晩飯食べつつ酒飲もうと思ったら、近所にはチェーン店の焼き鳥屋しかなかった。

f:id:kmaebashi:20190811201056j:plain

f:id:kmaebashi:20190811201709j:plain

f:id:kmaebashi:20190811202050j:plain

f:id:kmaebashi:20190811204530j:plain

f:id:kmaebashi:20190811205825j:plain

いくら翌日いっぱい走るからって、食いすぎである。

さて翌朝。AM5時すぎぐらいに出発です。

f:id:kmaebashi:20190812051734j:plain

ホテルを出るところ。写真の日付によると05:17。

f:id:kmaebashi:20190812052850j:plain

湖畔に出ました。

ビワイチのコースには、このように、青いマークが書いてあります…… まあだいたいは。ていうか琵琶湖大橋以北にはたいてい書いてあったけど、南の方はほとんどなかった気がする。

私の場合は彦根を起点に、反時計回りで周ります。ビワイチはたいてい反時計回りで回りますが、これは自転車は左側通行なので、左手に琵琶湖を臨みながら走り続けられるから、ということのようです。

f:id:kmaebashi:20190812052857j:plain

25kmほど走ったところの道の駅。06:30頃。給水はできたけど、時間が早すぎて食べ物屋は開いてなかった。

f:id:kmaebashi:20190812063005j:plain

事前にちょっと調べたブログ記事で、「1か所だけトンネルがあってそこが怖かった」とあったのですが、このトンネルはあっという間に終わってしまった。

f:id:kmaebashi:20190812063850j:plain

どうも、そのブログの方が怖く感じたトンネルは、青い印に沿って走ると迂回するようです。

ところでビワイチ関係なく、私のロードバイク(2017年の3月に購入)の総走行距離が1万キロを超えようとしていました。

f:id:kmaebashi:20190812065204j:plain

琵琶湖一周なので基本平坦なのですが、湖北のあたりにはちょっと山もあります。

f:id:kmaebashi:20190812065857j:plain

何やら工事中だったようですが、自転車は通れた。

f:id:kmaebashi:20190812070059j:plain

工事中エリアの中にもトンネルがありましたが、これもすぐに終わった。

f:id:kmaebashi:20190812070200j:plain

トンネルを抜けたところで、このロードバイクの走行距離が1万キロを突破しました!

f:id:kmaebashi:20190812070516j:plain

湖北のあたりはコンビニもほとんどなくて、そろそろ腹も減ったし困ったな、と思ったところでローソン発見。塩分補給も兼ねてこんなものを。

f:id:kmaebashi:20190812073135j:plain

上の写真で一緒に買ってるカロリーメイトは、その後のトイレ休憩の時に食べました。

f:id:kmaebashi:20190812095226j:plain

この時点で、09:49頃。位置はこのあたり。

f:id:kmaebashi:20190818232128j:plain

100kmちょい走ったところでカフェに入って昼食。近江牛ドッグ。

f:id:kmaebashi:20190812114533j:plain

いまここ。11:44。

f:id:kmaebashi:20190818232428j:plain

ここから琵琶湖大橋を渡ってしまえば近いのですが、がんばって南側も走る。

「自転車処どてるし」というスポーツバイクのパーツショップがあって、ソフトクリームが食べられるというので寄ってみる。

f:id:kmaebashi:20190812122501j:plain

お店の外観。

f:id:kmaebashi:20190812124009j:plain

さらに走って近江大橋。いつも京都に行くときはここを渡ります。この橋は、ほとんど琵琶湖の下端にかかっているので、ここで渡ってショートカットしてもよさそうなものなのですが、愚直に全部周ることにします。

f:id:kmaebashi:20190812140042j:plain

……で、だ、上で挙げた「輪の国びわ湖推進協議会」のページのマップを見てみると、

f:id:kmaebashi:20190818233124j:plain

こんなとこまで琵琶湖なの? ここって川じゃないの?

……と思いつつ、全部周る。

南郷洗堰というところで、琵琶湖というか、瀬田川を渡ります。

f:id:kmaebashi:20190812143844j:plain

いつも京都に行くときに寄るイオンモール草津店で銀だこ。あんまりここで時間を浪費すべきではなかったとは思うのですが。

f:id:kmaebashi:20190812152006j:plain

さて、ここから先は、いつも京都から帰るときに通るルートだ……と言えればよいのですが、いつも京都に行くときは、湖畔道路の曲がりくねった道を田舎道でショートカットしているわけで、今回は、そこも愚直に湖畔道路を走ります。

で、いつも湖畔道路から離れる分岐点ローソン(と私が勝手に呼んでいる)まで辿り着いた。あとは本当にいつものルート。

f:id:kmaebashi:20190812170232j:plain

173km地点のローソン。お茶を購入。

f:id:kmaebashi:20190812174319j:plain

182km地点くらいのセブンイレブンで最後の補給。さっきのローソンから10km走ってないですが、ローソンで食べ忘れたのと、この先しばらくコンビニもないはずなので(…と思っていたら、なんかファミマができてたようだ)。

f:id:kmaebashi:20190812180944j:plain

湖岸道路で日が暮れます。

f:id:kmaebashi:20190812183501j:plain

夕日に照らされる山。

f:id:kmaebashi:20190812183504j:plain

まあでも、暗くなる前に湖岸道路を脱出できて助かりました。湖岸道路は街灯とか全然なくて、ここをしょぼい自転車のライトで走りたくはないわけで。

朝、出発したホテルに到着。19:16、197.87km。

f:id:kmaebashi:20190812191644j:plain

このホテルで連泊できればよかったのですが、部屋が取れなかったので、この晩は別のホテルに泊まります。このホテルには荷物だけ預かっていただいたので、それを受け取って次のホテルまで自走。

f:id:kmaebashi:20190812195219j:plain

最終的な走行距離は、201.3kmとなりました。

当然、飲みに出ます。ホテルのそばにはまた焼き鳥屋しかなかった。

f:id:kmaebashi:20190812205426j:plain

f:id:kmaebashi:20190812205624j:plain

f:id:kmaebashi:20190812211251j:plain

f:id:kmaebashi:20190812211606j:plain

f:id:kmaebashi:20190812212952j:plain

f:id:kmaebashi:20190812214717j:plain

食いすぎである。(ついでに、飲みすぎである)

翌朝、彦根から名古屋まで80kmくらい、自走して帰るか新幹線で帰るかで迷いました。この時期の国道21号とか養老山脈とかの山道って私にとっては結構「走りたい道」で、距離的にもたいしたことはないのですが……

前の晩、あんなにビールを飲んだのにおしっこした覚えがないとか、翌朝出たおしっこがやけに色が濃かったとか、夜中に持病の外耳炎が急に痛くなったとかがあって、体が脱水状態になっている可能性を考え、おとなしく輪行で帰りました。世間がこれだけ熱中症熱中症と騒いでいる中、好き好んで自転車で走ってぶっ倒れたら誰も同情してくれないよね。

米原駅まで行ったら、レンタサイクルの貸し出しをしてました。レンタサイクルで走るような人が、もちろん1日で走るわけではないにせよ、200km走るって結構きついんじゃないかなあ。

f:id:kmaebashi:20190813122259j:plain

f:id:kmaebashi:20190813122305j:plain

琵琶湖大橋以南はクルマで渋滞してたりで走りにくかったので、またやるとしたら、琵琶湖大橋以北だけでいいかなあ……

 ところで、「輪の国びわ湖推進協議会」では琵琶湖1周の認定証を発行しているようですが、私にはやり方がよくわからなかった。Google先生が私の居場所を勝手にぶっこ抜いてタイムラインを作ってくれているので、これが証明になるでしょう。

f:id:kmaebashi:20190819001602j:plain

 

しまなみ海道にリベンジに行った……はずが

昨年、自転車乗りの聖地とも楽園とも言われるしまなみ海道にはじめて行ったのですが、当日は素晴らしく天気がよかったものの、折悪しくその1週間前の水害の爪痕が残っており、一部崖崩れしてたり、食べ物屋さんの類が断水で寄れなかったり、たいへん楽しかったのですがいまひとつ残念であったので、リベンジを誓ったのでした。

kmaebashi.hatenablog.com

kmaebashi.hatenablog.com

そして今年。去年と同様7月の3連休に宿を取り、行ったのですが、水害の爪痕こそなかったものの、今年は当日の天気が悪かった…… orz

7/13の朝。始発の新幹線(06:20)で名古屋駅から新尾道まで移動します。

f:id:kmaebashi:20190713053517j:plain

輪行袋に自転車詰める。

f:id:kmaebashi:20190713082007j:plain

福山で乗り換え。

f:id:kmaebashi:20190713084657j:plain

新尾道に着いたところ。既に降っている……
天気予報を見ながら、なんとか土曜の夕方ぐらいまでは持たないものかと思っていたのですが……

とはいえこの時点では本降りではなく、しまなみ海道を走り始めた時点では「時々雨がぱらつく」レベルでした。

f:id:kmaebashi:20190713090346j:plain

とにかく輪行解除して走り始めます。
昨年は、輪行袋新尾道駅のコインロッカーに置いていきました。この時期背中には何も背負いたくないし、写真にあるサドルバッグでは着替えを入れると一杯で輪行袋は入らないからです。しかし今年は、天気が天気なので、いつ輪行で帰りたくなるかわからないため、ヘルメット袋にしている簡易リュックで輪行袋を背負って走りました。結果的には正解だった。

f:id:kmaebashi:20190713091953j:plain

去年寄ったのと同じローソンで補給。去年は水はおひとり様2本までだったし断水でトイレも借りられなかったのですが、今年は当然そんな制限はなし。

f:id:kmaebashi:20190713093652j:plain

最初の島である向島までは船で渡ります。

f:id:kmaebashi:20190713095135j:plain

到着するとこ。

f:id:kmaebashi:20190713095355j:plain

そして、青い線。これを辿れば今治まで行ける。この場所に関する限り、ほぼ消えかけてますが。

f:id:kmaebashi:20190713100920j:plain

因島大橋。この橋は自転車は橋の下を通ります(去年の記事には写真もあります)。

f:id:kmaebashi:20190713104915j:plain

去年は開いてなかったお店で、

f:id:kmaebashi:20190713104330j:plain

はっさく大福をいただく。

f:id:kmaebashi:20190713111038j:plain

f:id:kmaebashi:20190713111402j:plain

生口橋で生地島に渡ります。

f:id:kmaebashi:20190713112811j:plain

去年と違い、トイレが機能しているのは助かります。
しかし、この辺から本格的に降り出した。

f:id:kmaebashi:20190713113755j:plain

「地だこ天」とかいうのをいただきます。

f:id:kmaebashi:20190713113953j:plain

こんなの。

f:id:kmaebashi:20190713114454j:plain

そしてジェラードも。デコみかん。

f:id:kmaebashi:20190713115602j:plain

ジェラードのお店は、「サイクルオアシス」にもなっていました。まあ空気入れ貸してくれるだけなんですけど(水やトイレは、何か買わないと使える感じじゃなかったなあ)。

f:id:kmaebashi:20190713122112j:plain

多々羅大橋

この辺からはもう、写真を撮る余裕もなくなってきています。

f:id:kmaebashi:20190713123511j:plain

サイクリストの聖地も、雨の中じゃねえ。

f:id:kmaebashi:20190713123555j:plain

説明。

f:id:kmaebashi:20190713131311j:plain

伯方島で遅めの昼食。しらす丼
びしょ濡れなので、食事にも気を使いますね……店の人がでっかいビニール袋を出してくれたので、それ椅子にしいて座る。

f:id:kmaebashi:20190713152539j:plain

ようやく今治です

1日のほとんどを雨の中走ってきたわけですが、まあ、濡れていくと決めてしまえば、雨の中でも走れないわけではありません。この時期ならカッパもなくても大丈夫。水を買うためにコンビニに寄る手間もかかりません。しかし、ブレーキの効きが悪くなって安全マージン下がりますし、それより何より、こんな雨の中走って楽しいかというと……

で、ホテルにチェックインして、後輪が跳ね上げた泥水で泥だらけになった背中の簡易リュックとサドルバックを風呂場で洗って干しつつ、サイクルウェアはコインランドリーで洗濯しつつ、風呂に入って、

f:id:kmaebashi:20190713182635j:plain

近所の飲み屋さんでようやくビール。

f:id:kmaebashi:20190713183911j:plain

カンパチのカマ焼きとか。

f:id:kmaebashi:20190713185421j:plain

刺身盛り合わせとか。

f:id:kmaebashi:20190713193445j:plain

じゃこ天とか。

で、早々に寝てしまって、

f:id:kmaebashi:20190714071014j:plain

翌朝の朝食は今治駅のお店でパニーニ。なかなか当たりであった。

さて、問題は、2日目、自転車で尾道まで戻るか、このまま今治駅から輪行で帰ってしまうかです。一応天気は回復基調にはあるようなので、駅前のホテルに戻り、しばらくうだうだしてからまた外に出てみたのですが…… まだまだ結構降っていたのであきらめて、結局今治駅から輪行で帰りました。

f:id:kmaebashi:20190714132249j:plain

帰りの新幹線で、駅弁。

f:id:kmaebashi:20190715130256j:plain

翌日は洗車しました。汚れているのもさることながら、チェーンの油もすっかり落ちてた。

今回、リベンジに失敗したので、また行かなきゃいけませんねえ。

 

React.jsでテトリス風ゲームを作ったよ

タイトルの通り、React.jsでテトリス風のゲームを作りました。

http://kmaebashi.com/programmer/reacttetris/index.html

ゲーム画面はこんな感じ。

f:id:kmaebashi:20190701005922p:plain

React.jsによるテトリス風ゲーム

遊び方やゲーム本体のページへのリンク、ソースプログラム一式は上記のページに記載しています。

見た目、前回のCanvasを使ったJavaScriptテトリスと何が違うんだ、と言われそうですが、中身は結構違うのです。いやほんと。

前回のJavaScriptテトリスではCanvasを使っていましたが、仮想DOMを使うReact.jsでゲームステージをCanvasで描いていたらメリットも使い方もわからないわけで、そこで今回はゲームステージはtableで作っています。Nextのところが同じクラスで作れたのがReact.jsのコンポーネント指向の強み? でも正直、自力でDOMをいじっても、うまく作れば同様のソースの再利用はできるのでは。

あとは正直、React.js版はCanvas版と比べて動きがちょっともっさりしている(特に↓押下時)気がするのですが、これは作りが悪いんでしょうか。

緑のアルプスあずみのセンチュリーライド(緑のAACR)に参加してきました

先週末は長野県安曇野市で行われる自転車イベント「緑のアルプスあずみのセンチュリーライド」(略称:緑のAACR)に参加してました。

aacr.jp

そういやここには書いてなかったですが、緑のAACRは昨年も参加しています。昨年は120kmでエントリしましたが(エントリ時点でそれしか空いてなかった)、今年は正しくセンチュリーライドである(?)160kmでエントリしました。

走るのは5/26(日)ですが、受付は前日ですし、スタートが朝の05:20なので前日から移動しておく必要があります。特急で輪行し、受付を済ませて地元の知人と酒飲んだりして、翌朝いよいよ出走です。

f:id:kmaebashi:20190526045547j:plain

スタート前。私は05:20スタートの組で、05:00スタートの組のスタートを待っているところです。

f:id:kmaebashi:20190526051002j:plain

いよいよスタート前です。前方にゲートが見えます。

痛ジャージと言うのか、萌え絵をあしらったジャージが結構ありましたね。スタート地点で見まわすと、(これは「萌え絵をあしらった」ものではないですが)チームFORTUNAジャージの人が3人いました。もちろん全員男性。

で、スタートすると写真を撮る余裕もそうそうないわけで、以後、写真はエイドでの補給食が主体になります。AACRはエイドの補給がおいしいので有名なのですよ。

f:id:kmaebashi:20190526063239j:plain

最初のエイド、あずみの公園穂高エイド。

自転車というのは風の影響をもろに受ける乗り物なので、ここに来るまで、ちょくちょく向かい風になるのが結構辛かったなあ。

120kmで参加すると、あずみの公園がスタート地点になります。

で、ここの補給食。

f:id:kmaebashi:20190526063516j:plain

米粉パンに、

f:id:kmaebashi:20190526063709j:plain

信濃銘菓あずさ。朝飯あんなに食ってくる必要はなかったな。

穂高エイドから20kmばかり走ると、あずみの公園大町エイドです。

わーいエイドだー、と公園に入ると、そこから結構な上り坂が続くというだまし討ちのようなエイド。

f:id:kmaebashi:20190530002238j:plain

ろんぐらいだぁす!」3巻より

※寄らない、というのは、実際のAACRではできないと思う……

で、エイドの風景。

f:id:kmaebashi:20190526074016j:plain

伝説の味噌おにぎりに、

f:id:kmaebashi:20190526074113j:plain

漬物バイキング。

f:id:kmaebashi:20190526074448j:plain

この時点ではまだそんなに暑くはなかったのですが(長野の山の中、朝の08:00過ぎですし)、この日は全国的に猛暑が予想された日なので、塩分も補給しないと、ですよね。

f:id:kmaebashi:20190526075522j:plain

「駐輪場」とあるのに実態はただの芝生で自転車が横倒しに置いてある、というのは、ロードバイクにあまり縁のない人には異様な光景に見えると思いますが、みんなスタンド付いてないんだからしょうがない。

大町エイドから次の大町木崎湖エイドまでの間で、北アルプスがきれいだったので1枚。

f:id:kmaebashi:20190526082117j:plain

大町エイドと木崎湖エイドの間の距離は短く、ほんの10kmちょい走れば到着です。

f:id:kmaebashi:20190526083224j:plain

そうめんに、

f:id:kmaebashi:20190526083406j:plain

水ようかん。

去年は120kmだったので早いうちにここに着いて、朝方雨が降ったりして寒かったこともあり、寒いのに冷たいものばかりかよと思ったものですが、今年は十分暑かった……

次のエイドまでの間に、また風景を1枚。

f:id:kmaebashi:20190526092538j:plain

次のエイドは白馬エイドです。

f:id:kmaebashi:20190526101946j:plain

f:id:kmaebashi:20190526102133j:plain

紫米おにぎりに、

f:id:kmaebashi:20190526102601j:plain

豚汁。

白馬エイドで出発前に撮った風景を何枚か。

f:id:kmaebashi:20190526104204j:plain

f:id:kmaebashi:20190526104207j:plain

f:id:kmaebashi:20190526104214j:plain

f:id:kmaebashi:20190526104327j:plain

f:id:kmaebashi:20190526104356j:plain

白馬エイドを出てすぐのところで、白馬のジャンプ台が見えます。

f:id:kmaebashi:20190526105514j:plain

これじゃ小さいので望遠にしようと思ったら、なんかスマホのカメラは連写モードになった。機械オンチで悲しい。

f:id:kmaebashi:20190526115005j:plain

公式のエイドでも何でもないですが、AACR参加者には有名なヤマザキYショップ。ここのおやきは外せないよね。

f:id:kmaebashi:20190530005623j:plain

ろんぐらいだぁす!」3巻表紙より

去年はトイレを借りたくて偶然寄ったのですが、160km参加者となると皆さんご存じのようで。

f:id:kmaebashi:20190526115013j:plain

f:id:kmaebashi:20190526115608j:plain

オーソドックスに野沢菜をいただきました。

ここから次のエイド、中山高原エイドまでが大変でした。延々と続く上り坂。

ようやく辿り着いてみたら、なんかエイドは大行列。

f:id:kmaebashi:20190526123719j:plain

f:id:kmaebashi:20190526124449j:plain

蕎麦の薄焼きに、

f:id:kmaebashi:20190526124656j:plain

冷奴。この冷奴の提供に手間取って大行列ができていたらしい。

どっかで見たような味噌おにぎりもありますが、大町エイドで残ったものを運んできたようです。私は食べませんでしたが、漬物もありました。

ここからは下りです。途中、「4キロ延々下り」という区間があり、ここは楽しい。

ところで今回走りながら自分に課したルールがあって、それは「抜くなら上り坂で抜け」というものです。上り坂をひいこら登っている時に抜くのは私より遅い人を抜くことが多いでしょうが、下り坂で遅い人はおそらく慎重な人なのであり、そういう人を抜いてイキっていてもしょうがないよね、と。おおむね守ったつもり。

最終エイドは安曇野エイドです。

f:id:kmaebashi:20190526142706j:plain

リンゴジュースと、あとゴマおはぎもあったのですが、そちらは写真を撮り忘れました。

120kmならここから6.4kmも走ればゴールなのですが、160kmだと20kmほど走る必要があります。この区間は、激坂はないもののじんわりと登りで、信号やら一時停止が多く、なかなかスピードが出せない区間ではあったのですが、黙々と漕ぎつづければ

f:id:kmaebashi:20190526155037j:plain

ゴール!!

f:id:kmaebashi:20190526155405j:plain

完走証。

f:id:kmaebashi:20190526155345j:plain

完走すると「おめでタイ焼き」がもらえます。これ、去年はアイスクリーム入りだと思ったのですが、今年は普通のたい焼きだった。あれ?

公式が事前に配布している目安タイムというのがあって、

f:id:kmaebashi:20190530011802p:plain

おやきを食べていた白馬→中山高原のところでちょっと遅れましたが(原因はおやきのせいだけではなく上り坂も大きかったと思う)、おおむねスケジュール通りにゴールできました。

ところで、160kmとはいうものの、実際の距離は上の公式の累計距離を見ても156.2kmです。まあこれぐらいはおまけでよいとして、ただ私のサイコンでは

f:id:kmaebashi:20190526162412j:plain

153.86kmになっていました…… 途中、速度計が明らかにおかしくなったのを目撃したりもしたので、輪行でセンサーがずれるとかして、私のサイコンがちょっと拾い損ねてたのかな。

いずれにしても、ゴールしてから松本駅まで自転車で行かなければ帰れないわけで、

f:id:kmaebashi:20190526184130j:plain

160kmは超えます。

で、松本駅に行く途中のスーパー銭湯で汗を流して着替えて、松本駅で自転車を輪行袋に詰めて、みどりの窓口で席を取ったら2時間後。まあ、そうなったらビール飲むしかないよねー。

f:id:kmaebashi:20190526183943j:plain

クラフトビールとか

f:id:kmaebashi:20190526185031j:plain

フライドチキンとか

f:id:kmaebashi:20190526185706j:plain

名前忘れたけど緑色のビールとか

f:id:kmaebashi:20190526191736j:plain

ピザとか

f:id:kmaebashi:20190526192028j:plain

穂高地ビールとか。

で、最寄り駅まで帰りついて輪行解除して、酒入ってるので押して帰りながら近所の店でもうちょっと飲んだりして、翌日の月曜は疲れ果てていることがわかっていたのであらかじめ有休をとっておきました。

で、洗濯とかしながらのんびりしつつ、自転車のクランクを回してみたら回らない。輪行でディレーラーでも壊したかと焦りましたが、輪行解除が雑すぎてスプロケにチェーンがはまっていなかった。やっぱ疲れていたよなあ。

昨年に引き続き、たいへん楽しいライドでした。また来年も行かざるを得ませんね。

(ただ、160kmだと私の走力ではやっぱりちょっと余裕がないので、120kmぐらいでもよいかも)

JavaScriptでテトリス風ゲームを作ったよ

ずいぶん久々の更新になってしまいました……

という話はさておき、タイトルの通り、JavaScriptテトリス風のゲームを作りました。

http://kmaebashi.com/programmer/tetris/index.html

ゲーム画面はこんな感じ。

f:id:kmaebashi:20190519230449p:plain

遊び方やゲーム本体のページへのリンク、ソースプログラム一式は上記のページに記載しています。

テトリスのひとつやふたつ、今時中学生でも作りそうですが、私自身は仕事でWebアプリを作ってもフロントエンドにかかわることが少ないので、ES6の勉強がてら作ってみました。ES6なのでIEでは動きませんが、今時のたいていのブラウザなら動くと思います。さすがにもう、趣味的な用途では、IEは無視していいよね……*1

生のJavaScriptで、画面描画にはCanvasを使っています。React.jsもVue.jsも使ってません。

しかし何だ、たったこの程度のプログラム(JavaScriptは320行)でさえ、「型なし言語逝ってよし!」と叫びたくなった。ひとりで書いていてさえこれだ。

*1:「趣味的な用途」以外では、無視できないと、今でも思っています。不本意ながら。