点とベクトルと座標変換

はじめに

研究室で座標変換について説明する機会が何回かありました. 本記事ではそこで説明した内容をまとめます.

本記事ではまず,「点」と「ベクトル」を定義します. 次に,「座標」と「座標系」を定義します. そして,行列を用いた座標変換について解説します.

点とベクトル

定義

点とは位置を持つものです. 以下では点を太字の文字(例えば, 𝐚 )で表すことにします.

ベクトルとは向きと大きさをものです. 以下ではベクトルを矢印のついた文字(例えば, v )で表すことにします.

以下に点とベクトルの例を示します.

点とベクトルの例
点とベクトルの例

演算

ベクトルの実数倍

ベクトル v の実数 kkv は次のようなベクトルを表します.

  • k>0 の場合: v と同じ向きで大きさを |k| 倍にしたベクトル
  • k<0 の場合: v と逆の向きで大きさを |k| 倍にしたベクトル
  • k=0 の場合:大きさが0のベクトル
ベクトルの和

ベクトル u とベクトル v の和 u+v は次のようなベクトルを表します.

  • u の終点と v の始点を一致させるように uv を平行移動させたときに, u の始点から v の終点へ向かうベクトル
点の平行移動

𝐚 のベクトル v による平行移動 𝐚+v は次のような点を表します.

  • 𝐚 から v の方向に v の大きさの分だけ移動した先の点

以下に点とベクトルの演算の例を示します.

点とベクトルの演算の例
点とベクトルの演算の例

座標と座標系

点の位置を数値を用いて表現することを考えます. 座標系とは点の位置を数値を用いて表現するときに基準となるものです. 座標系は一つの点といくつかのベクトルで構成されます. 座標系を構成する点は原点を,ベクトルは座標軸を表します. ある点の座標とはその点の位置を表現する数値の組です.

具体的には,点 𝐨 とベクトル v0v1 で構成された座標系を (𝐨,v0,v1) とし,ある点 𝐚𝐨v0v1 と実数 a0a1 を用いて 𝐚=𝐨+a0v0+a1v1 と表せる時,座標系 (𝐨,v0,v1) を基準とした点 𝐚 の座標は (a0,a1) となります.

以下に座標系と座標の例を示します.

座標系と座標の例
座標系と座標の例

座標変換と座標変換行列

座標変換とは,ある座標系を基準とした点の座標を,別の座標系を基準とした座標として表現し直すことです. ここでは,行列を用いて座標変換を行う方法と,逆に座標変換前後の座標から座標変換で用いられた行列を求める方法を解説します.

行列を用いて座標変換を行う

以下の点と行列が与えられたとします.

  • 𝐚 :座標系 (𝐨,v0,v1) を基準とした座標が (a0,a1) である.
  • 行列 MM=(m00m01m02m10m11m12001)

この時, 𝐩=𝐨+m02v0+m12v1u0=(m00+m02)v0+(m10+m12)v1u1=(m01+m02)v0+(m11+m02)v1 を満たす別の座標系 (𝐩,u0,u1) を考え,その座標系を基準とした点 𝐚 の座標 (b0,b1) を次のように求めることができます. (b0b11)=M(a0a11)=(m00m01m02m10m11m12001)(a0a11)=(m00a0+m01a1+m02m10a0+m11a1+m121)

座標変換で用いられる行列を座標変換行列と呼びます.

以下に行列を用いて座標変換を行う例を示します.

行列を用いて座標変換を行う例
行列を用いて座標変換を行う例

座標変換行列を求める

ある2つの座標系 (𝐨,v0,v1)(𝐩,u0,u1) と以下の3点が与えられたとします.

  • 𝐚0
    座標系 (𝐨,v0,v1) を基準とした座標: (a00,a10)
    座標系 (𝐩,u0,u1) を基準とした座標: (b00,b10)
  • 𝐚1
    座標系 (𝐨,v0,v1) を基準とした座標: (a01,a11)
    座標系 (𝐩,u0,u1) を基準とした座標: (b01,b11)
  • 𝐚2
    座標系 (𝐨,v0,v1) を基準とした座標: (a02,a12)
    座標系 (𝐩,u0,u1) を基準とした座標: (b02,b12)

この時,座標系 (𝐨,v0,v1) を基準とした任意の点の座標を,座標系 (𝐩,u0,u1) を基準とした座標に変換する座標変換行列 M を次のように求めることができます.

まず, 𝐚0𝐚1𝐚2 を行列 M を用いてそれぞれ座標変換すると 𝐛0𝐛1𝐛2 となることから, (b00b101)=M(a00a101)(b01b111)=M(a01a111)(b02b121)=M(a02a121) が成り立つはずです. これらをまとめて (b00b01b02b10b11b12111)=M(a00a01a02a10a11a12111) と表すことができ,以下のように M について解くことで座標変換行列を求めることができます. M=(b00b01b02b10b11b12111)(a00a01a02a10a11a12111)1

以下に座標変換行列を求める例を示します.

座標変換行列を求める例
座標変換行列を求める例

まとめ

  • まず,「点」,「ベクトル」,「座標系」,「座標」を定義した.
  • 座標変換行列を用いて点を座標変換する方法について解説した.
  • 座標変換前後の点の座標から座標変換行列を求める方法について解説した.
  • 数式はMathMLで書いた.

Swingベースのシンプルなスケッチアプリ

概要

JavaFXjdkに同梱されなくなったため,今後,JVM上で動作するGUIアプリの開発は,Swingベースで行うことにしようと思います. そこで,シンプルなスケッチアプリを題材として,SwingベースのGUIアプリのテンプレートを作成しました. 本記事では,まずそのソースコードと動作例を示した上で,以下の点に焦点を当てて解説します.

  • ウィンドウの表示
  • イベントリスナの登録
  • 折れ線の描画

ソースコード

Kotlin

Java

動作例

f:id:Jumpaku:20200526184148p:plain
動作例

解説

プログラムを起動すると以下の流れで処理が実行されます.

  1. mainが実行される
  2. SwingUtilities.invokeLaterが実行される
  3. SwingSketchApprunが実行される

ウィンドウの表示

JFrameはウィンドウを表すクラスです. // ウィンドウの表示の部分ではJFrameインスタンスを生成してウィンドウを表示しています. 具体的には以下の項目を設定した後,setVisibletrueを渡すことでウィンドウが表示されます.

  • ウィンドウのタイトル:JFrameのコンストラクタに渡した文字列はウィンドウのタイトルとなります.
  • 閉じるボタンを押した時の動作:setDefaultCloseOperationJFrame.EXIT_ON_CLOSEを渡すとウィンドウの閉じるボタンを押した時にプログラムを終了します.
  • ウィンドウが持つコンテンツ:addはウィンドウにコンテンツを追加します.
  • ウィンドウのサイズ:packを呼び出すとウィンドウのサイズをコンテンツに応じて調整します.

イベントリスナの登録

アプリケーションにユーザの入力に応じた処理を実行させるためにはイベントリスナを登録する必要があります. イベントとはプログラムの状態の変化をトリガーとして生成されるインスタンスです. 例えば,ユーザがマウスのボタンを押すことでプログラム内のマウス押下状態が変化した時やユーザがマウスドラッグすることでプログラム内のマウス位置が変化した時にはMouseEventインスタンスが生成されます. イベントリスナとはイベントが生成された時にそのイベントを引数にして実行されるメソッドまたはそのようなメソッドを持つクラスです.

// イベントリスナの登録の部分ではイベントリスナとしてMouseInputListenerインスタンスlistenerを生成し,これをpanelに登録しています. これにより,panelの上でユーザがマウスのボタンを押した時やマウスドラッグした時に,そのイベントに応じたlistenerのメソッドが実行されるようになります.

折れ線の描画

JPanelはコンテンツを追加できる汎用的なパネルを表すクラスです. JPanelpaintをオーバーライドすることで図形を描画することができます. ここではシンプルなスケッチを実現するためにユーザのドラッグにより入力された点列を折れ線として描画します. 具体的には// 折れ線の描画の部分で示すように,paintに引数として渡されるGraphics2Dインスタンスに対して,Point2Dのリストとして表された点列をLine2Dとして表される線分で繋いでいます.

Graphics2Dの使い方は以下の記事が参考になると思います. jumpaku.hatenablog.com

Jumpaku Dinner

Jumpaku Dinnerのデモンストレーション画像
Jumpaku Dinnerのデモンストレーション
https://dinner.jumpaku.net

はじめに

夜ご飯決定アプリ「Jumpaku Dinner」を開発・公開し,その成果をLT大会で発表しました. 以下では,

  • Jumpaku Dinnerとは何か
  • どのように使えば良いか
  • どのように動作しているか
  • なぜ制作したか
  • 感想
  • 質疑応答

などを述べます.

Jumpaku Dinnerとは何か

Jumpaku Dinnerは,ある日の夜ご飯として何を作るかを決定するためのWebアプリケーションです. ページ(https://dinner.jumpaku.net)を開くと,ランダムに一つの料理が選択され,その料理の料理名,写真,材料,一言コメントが表示されます.

どのように使えば良いか

「今日の夜ご飯何食べたい?」「なんでも良い」「なんでも良いは困る!」

そんな会話に覚えはありませんか? 夜ご飯で作るものを決められない親,食べられればなんでも良い子供,そういった方がこのアプリに従うことで,喧嘩することなく夜ご飯を決定することができます. 他にも自炊したいけど,何を作ったら良いか分からない一人暮らしの大学生も同様です.

使い方は以下の通りで非常に簡単です.

  1. 今日の夜ご飯はJumpaku Dinnerに従うと心に決める.
  2. Jumpaku Dinnerのページを開く.
  3. Jumpaku Dinnerに提示された夜ご飯を作って食べる.

どのように動作しているか

https://dinner.jumpaku.netで公開しているJumpaku Dinnerの概要を以下に示します.

Jumpaku Dinnerの概要を表す図
Jumpaku Dinnerの概要
Jumpaku Dinnerのフロントエンドは,TypeScriptとReactで記述され,webpackによりビルドされます. Jumpaku Dinnerのバックエンドではhttpdが動作しており,ビルドされたリソースをサーブしています. これらをまとめたDockerイメージはDocker Hubで公開しています.

なぜ制作したか

2020年5月頃より,私は自炊した料理の写真をTwitter(Jumpaku@Jumpaku)にアップロードするようになりました. そして写真は2020年9月頃には50枚程度まで増えていましたが,一方で,作りたい料理は無くなっていきました.

また,その頃,プログラミングに関する活動として競技プログラミングAtCoder)やVPSの管理などしかしておらず,久しくアプリ開発をしていませんでした. そのため,私の開発欲は高まっていました.

そんな中で,私のフォロワのツイートによりあるイベント(【学生限定】秋のLT大会 2020 Online)が開催されること知りました. cist-lt.connpass.com そしてこのLT大会の開催日は,私がそれを知った日の翌日でした.

増えた写真,減りゆく自炊,高まる開発欲,迫る締め切り,VPSの活用,そして発表機会. その時,すべての点が線でつながりました.

「明日までに一人ハッカソンで夜ご飯決定アプリ「Jumpaku Dinner」を制作してVPSで公開し,LT大会で発表しよう!」

そう思い至った私はすぐに参加登録をし,環境構築から開発をスタートしたのでした.

以上の経緯により,Jumpaku Dinnerを制作しました.

感想

  • 自分で撮った写真を活用したアプリを制作できて良かった.
  • 自分にとって役に立つアプリを制作できて良かった.
  • VPSで動作させるサービスが増えて良かった.
  • 締め切りに追われながらの一人ハッカソンは,スリリングで楽しかった.
  • 完成した成果を発表でき,さらにTwitter等で反応を頂けて嬉しかった.

質疑応答

LT大会で発表した際に,「短期間で完成させるためにはどうしたら良いか?」という質問をいただきました. これに対し,「短期間で完成させられる程度まで要求を低くしていけば良い.」と答えました. このことは,開発時に意識してやっていたわけではないのですが,質問されたことにより,何かを完成させるために大事なことの一つとして自覚することができました. こういったことも含めて発表して良かったと思います.

まとめと展望

  • 夜ご飯決定アプリ「Jumpaku Dinner」を制作しました.
  • 自分のVPSでJumpaku Dinnerを公開しました.
  • 開発成果をLT大会で発表しました.

今後の展望として料理に関するデータをデータベースで管理するようにしたいと考えています.

リンク

C++におけるabs関数のオーバーロードについて調べた

目的

https://ja.cppreference.com/w/cpp/header によると,cmathcstdlibなどのC互換ヘッダはstd名前空間で宣言した関数をグローバル名前空間でも宣言するかもしれない. 実際,以下のソースコードコンパイルしてみると,グローバル名前空間abs関数とstd名前空間abs関数の両方を参照できることが分かる.

#include <cmath>
#include <cstdlib>
#include <iostream>

using ll = long long int;

int main() {
  ll x = 1e9;
  std::cout << ::abs(x * x) << std::endl;
  std::cout << std::abs(x * x) << std::endl;
}
g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment  -o main main.cpp

しかし,以下のように生成された実行ファイルを実行してみると,グローバル名前空間abs関数とstd名前空間abs関数が異なる結果を出力していることが分かる.

  • 実行コマンド
./main
  • 実行結果
1486618624
1000000000000000000

このプログラムは(109) * (109)の絶対値を求めようするものであるが,グローバル名前空間abs関数を使うと正しい値を求められない.

本記事では,グローバル名前空間abs関数とstd名前空間abs関数がどのように解決されるかを確かめ,この問題の対策を考える.

検証

https://cpprefjp.github.io/reference/cstdlib.html によると,cstdlibint型の絶対値を求めるint abs(int)を宣言している. また,https://cpprefjp.github.io/reference/cmath/abs.html によると,cmathint abs(int)に加えて,long long int型の絶対値を求めるlong long int abs(long long int)を宣言している.

そこで,グローバル名前空間abs関数とstd名前空間abs関数に対して,上のプログラムのようにlong long int型の引数を渡した時,これらの関数がそれぞれint abs(int)long long int abs(long long int)のどちらに解決されるのかを確かめるために,以下のソースコードコンパイルしてみた.

ソースコード

#include <cmath>
#include <cstdio>
#include <cstdlib>

#include <type_traits>
using std::is_same_v, std::declval;
using ll = long long int;

int main() {
  static_assert(is_same_v<decltype(::abs(declval<ll>())), int>);
  static_assert(is_same_v<decltype(std::abs(declval<ll>())), ll>);
}

コンパイル

検証は以下の環境で行った.

以下のコマンドによりコンパイルを行い,実行ファイルmainを得た.

g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment  -o main main.cpp

結果

上のソースコードコンパイルに成功したことから,以下のことが分かる.

  • グローバル名前空間abs関数にlong long int型の引数を渡すとint abs(int)に解決される.
  • std名前空間abs関数にlong long int型の引数を渡すとlong long int abs(long long int)に解決される.

考察

検証結果から最初に示したプログラムで,グローバル名前空間abs関数を使った場合に正しい値を求められなかった原因は次のようなものだと考えられる. グローバル名前空間abslong long int型の引数を渡すと,absint abs(int)に解決されるため,引数がint型に変換される. この時,(109) * (109) == 1018はint型で表現するには大きすぎるためオーバーフローが発生し,正しくない値がabsに渡ってしまう.

対策

グローバル名前空間int abs(int)関数にlong long int型の引数を渡してしまう誤りはコンパイルエラーとして検出できない. また,long long int型の値の絶対値を求めるときはllabsを使うように意識するという方法や,abs関数の前に常に「std::」を付けるように意識するという方法は,ミスの無い完璧な人間は存在しないということを考えれば,対策として十分とは言えない. したがって,一般的な対策はとても難しいと考えられる.

しかし,対象をプログラミングコンテストなどに限れば以下のような対策が実施可能である. 予めテンプレートとなるソースコードを用意しておくことが可能なAtCoderhttps://atcoder.jp)のようなプログラミングコンテストでは,そのテンプレートに次の記述を追加しておくことで,グローバル名前空間int abs(int)関数にlong long int型の引数を渡してしまう誤りをなくすことができる.

#include<cmath>
using std::abs;

このusing宣言により,グローバル名前空間std名前空間abs関数が導入されるため,long long int型をグローバル名前空間abs関数の引数にしようとした場合でもlong long int abs(long long int)が参照されるようになる. 実際,最初のソースコードにこれを追加して実行すると正しい結果が得られる.

#include <cmath>
#include <cstdlib>
#include <iostream>

using ll = long long int;

using std::abs;

int main() {
  ll x = 1e9;
  std::cout << ::abs(x * x) << std::endl;
  std::cout << std::abs(x * x) << std::endl;
}
  • 実行結果
1000000000000000000
1000000000000000000

JavaFXのウィンドウやSVG画像への図の描画

本記事はMuroran Institute of Technology Advent Calendar 2019 12/14の記事です.

adventar.org

概要

本記事では

  1. JavaFXウィンドウへの図の描画,
  2. SVG画像への図の描画,
  3. JavaFXウィンドウとSVG画像への同じ図の描画

について具体的なGradleプロジェクトの例を用いて説明します.

JavaFXJavaGUIライブラリの一つです. JavaFXはJava8で標準ライブラリに追加されてJDKの一部となりましたが,Java11からはJDKに同梱されなくなり,代わりにOpenJFXを利用することができるようになりました. 1.では,JavaFXCanvasクラスに図の描画を行うためにorg.jfreeのFXGraphics2DというライブラリのFXGraphics2Dクラスを利用します. FXGraphics2Dクラスは標準ライブラリのGraphics2Dクラスを継承しているため,Graphics2Dクラスと同じように扱うことができます.

SVG画像はベクタ画像(品質の劣化なくサイズを変更可能な形式の画像)の一つです. SVG画像の形式はXML形式で,仕様はW3Cによって勧告されています. 2.では,SVG画像に図の描画を行うためにorg.jfreeのJFreeSVGというライブラリのSVGGraphics2Dクラスを利用します. SVGGraphics2Dクラスも標準ライブラリのGraphics2Dクラスを継承しており,Graphics2Dクラスと同じように扱うことができます.

3.では,FXGraphics2DクラスとSVGGraphics2DクラスがGraphics2Dクラスを継承していることを生かたポリモーフィックなプログラムによって,JavaFXウィンドウとSVG画像への同じ図の描画を行います.

1.,2.,3.はOpenJFX,FXGraphics2D,JFreeSVGといった標準のJDKに含まれないライブラリを利用しますが,これらのライブラリの管理を含めたプロジェクトのビルドを行うために,本記事ではGradleというビルドツールを使用します. Gradleはbuild.gradleという設定ファイルに従って,コンパイル,ライブラリの用意,実行といった操作を自動的に行うソフトウェアです.

以下では,まず,本記事の実行環境について説明し,Graphics2Dクラスの基本的な使い方を示します. その後1.,2.,3.についてそれぞれ,Gradleプロジェクトの例を示します. 本記事ではJavaの環境としてJava13を利用しました.

実行環境

本記事のプログラムは,次のようにGitHubリポジトリからクローンして実行することができます.

git clone https://github.com/Jumpaku/AdventCalendar_2019-12-14.git
cd AdventCalendar_2019-12-14

# JavaFXウィンドウへの図の描画
cd example1
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

# SVG画像への図の描画
cd ../example2
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

# JavaFXウィンドウとSVG画像への同じ図の描画
cd ../example3
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

ただし,Java13とGitがインストールされていることが前提となります.

このリポジトリは以下のように3つのプロジェクトexample1,example2,example3で構成されています. f:id:Jumpaku:20191213230441p:plain 例えば,example1ディレクトリで./gradlew runを実行すると,Gradleがexample1/build.gradleに書かれたプロジェクトの設定に従ってプロジェクトをビルドし,起動します. プログラムが起動されるとexample1/src/main/java/example1/Main.javamainメソッドが呼ばれます. このmainメソッドはFXGraphics2Dクラスを利用します. FXGraphics2Dクラスはorg.jfreeのFXGraphics2Dというライブラリに含まれています. 必要となるライブラリをbuild.gradleに書いておくと,ビルドする時にGradleによってダウンロードされてライブラリを利用できるようになります.

Graphics2Dの基本的な使い方

Graphics2Dの基本的な使い方として

  • 基本図形のストローク描画とフィル描画
  • パス操作
  • 領域操作
  • アフィン変換

を示します.

基本図形のストローク描画とフィル描画

    public static void exampleDrawAndFill(Graphics2D g) {
        // 楕円を表現するEllipse2Dオブジェクトを生成する
        // 基本図形は他にLine2D(線分),Rectangle2D(長方形),Arc2D(弧)などもある
        Ellipse2D ellipse = new Ellipse2D.Double(0.0, 25.0, 100.0, 50.0);
        // Ellipseオブジェクトの内部の塗りつぶし色を設定する
        g.setColor(new Color(255, 75, 0));
        // Ellipseオブジェクトの内部を塗りつぶす
        g.fill(ellipse);
        // Ellipseオブジェクトの輪郭線の色を黒に設定する
        g.setColor(Color.BLACK);
        // Ellipseオブジェクトの輪郭線を太さ1の線に設定する
        g.setStroke(new BasicStroke(1f));
        g.draw(ellipse);
    }

パス操作

    public static void examplePath(Graphics2D g) {
        // 複雑なパス図形を表現できるPath2Dオブジェクトを生成する
        Path2D curve = new Path2D.Double();
        // Path2Dオブジェクトの開始点を設定する
        curve.moveTo(125.0, 25.0);
        // Path2Dオブジェクトの開始点に,3次Bezier曲線を接続する
        curve.curveTo(200.0, 25.0, 100.0, 75.0, 175.0, 75.0);
        // Path2Dオブジェクトの輪郭線を描画する
        g.setColor(new Color(3, 175, 122));
        g.setStroke(new BasicStroke(1f));
        g.draw(curve);
        // Path2Dオブジェクトを生成する
        Path2D polyline = new Path2D.Double();
        // Path2Dオブジェクトの開始点を設定する
        polyline.moveTo(125.0, 25.0);
        // Path2Dオブジェクトの開始点に,線分を接続する
        polyline.lineTo(200.0, 25.0);
        // Path2Dオブジェクトに,さらに線分を接続する
        polyline.lineTo(100.0, 75.0);
        // Path2Dオブジェクトに,さらに線分を接続する
        polyline.lineTo(175.0, 75.0);
        // Path2Dオブジェクトの輪郭線を黒い太さ1線に設定し,描画する
        // BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUNDは線分の端点や接続部分を丸くするための設定
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(polyline);
    }

領域操作

    public static void exampleArea(Graphics2D g) {
        // 3個のEllipse2Dオブジェクトを生成する
        Ellipse2D ellipse0 = new Ellipse2D.Double(0.0, 100.0, 100.0, 50.0);
        Ellipse2D ellipse1 = new Ellipse2D.Double(0.0, 100.0, 50.0, 100.0);
        Ellipse2D ellipse2 = new Ellipse2D.Double(0.0, 100.0, 50.0, 50.0);
        // 3個のEllipse2Dオブジェクトの和集合領域を表現するAreaオブジェクトを生成する
        Area union = new Area(ellipse0);
        union.add(new Area(ellipse1));
        union.add(new Area(ellipse2));
        // 3個のEllipse2Dオブジェクトの共通集合領域を表現するAreaオブジェクトを生成する
        Area intersection = new Area(ellipse0);
        intersection.intersect(new Area(ellipse1));
        intersection.intersect(new Area(ellipse2));
        // 和集合領域と共通集合領域を塗りつぶす
        g.setColor(new Color(77, 196, 255));
        g.fill(union);
        g.setColor(new Color(0, 90, 255));
        g.fill(intersection);
        // 和集合領域と共通集合領域の輪郭線を描画する
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(union);
        g.draw(intersection);
    }

アフィン変換

    public static void exampleTransform(Graphics2D g) {
        // 弧を表現するArc2Dオブジェクトを生成する
        Arc2D arc = new Arc2D.Double(-1.0, -1.0, 2.0, 2.0, -45.0, 270.0, Arc2D.PIE);
        // アフィン変換を表現するAffineTransformオブジェクトを生成する
        AffineTransform t = new AffineTransform();
        t.translate(150.0, 150.0); // 平行移動
        t.rotate(Math.PI / 2); // 回転
        t.scale(50.0, 50.0); // 拡大
        // Arc2Dオブジェクトにアフィン変換した図形オブジェクトを生成する
        Shape transformed = t.createTransformedShape(arc);
        // アフィン変換した図形オブジェクトを塗りつぶす
        g.setColor(new Color(255, 241, 0));
        g.fill(transformed);
        // アフィン変換した図形オブジェクトの輪郭線を描く
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(transformed);
    }

JavaFXウィンドウへの図の描画

JavaFXウィンドウへの図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
    // JavaFXを利用するためにOpenJFXのプラグインを追加する
    id 'org.openjfx.javafxplugin' version '0.0.8'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// JavaFXに関する設定をする
javafx {
    // 使用するJavaFXのバージョンを指定する
    version = "13.0.1"
    // JavaFXで使用するモジュールを指定する(モジュールはJava9の機能)
    modules = ['javafx.controls', 'javafx.swing']
}
// example2.Mainクラスをメインクラスとして指定する
mainClassName = 'example1.Main'
// FXGraphics2Dライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8'
}

Main.java

package example1;
/*import...*/
public class Main {
    /**
     * JavaFXウィンドウに図を描画する
     * さらに描画した図をpng画像としてファイルに書き出す
     * @param args
     */
    public static void main(String[] args) {
        // Appクラスを指定してアプリケーションを起動する
        Application.launch(App.class, args);
    }
    public static class App extends Application {
        /**
         * アプリケーション起動時に呼び出される
         * @param primaryStage
         */
        @Override
        public void start(Stage primaryStage) throws IOException {
            // Canvasオブジェクトを用意する
            int width = 320;
            int height = 240;
            Canvas canvas = new Canvas(width, height);
            // CanvasオブジェクトのGraphicsContext2DオブジェクトをFXGraphics2Dクラスのコンストラクタに渡す
            GraphicsContext ctx = canvas.getGraphicsContext2D();
            Graphics2D g = new FXGraphics2D(ctx);
            // Graphics2Dクラスを通してCanvasオブジェクトに図を描画する
            exampleDrawAndFill(g);
            examplePath(g);
            exampleArea(g);
            exampleTransform(g);
            // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する
            primaryStage.setScene(new Scene(new Pane(canvas)));
            primaryStage.show();
            // CanvasオブジェクトのPNG画像を書き出す
            SnapshotParameters params = new SnapshotParameters();
            WritableImage img = canvas.snapshot(params, new WritableImage(width, height));
            File file = new File("example.png");
            ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file);
        }
    }
    public static void exampleDrawAndFill(Graphics2D g) {/*...*/}
    public static void examplePath(Graphics2D g) {/*...*/}
    public static void exampleArea(Graphics2D g) {/*...*/}
    public static void exampleTransform(Graphics2D g) {/*...*/}
}

実行結果

example1ディレクトリにおいて,./gradlew runを実行して表示されたウィンドウを以下に示します.

f:id:Jumpaku:20191212024459p:plain

また,書き出されたPNG画像(example1/example.png)を以下に示します.

f:id:Jumpaku:20191212024023p:plain

SVG画像への図の描画

SVG画像への図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// example2.Mainクラスをメインクラスとして指定する
mainClassName = 'example2.Main'
// JFreeSVGライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4'
}

Main.java

package example2;
/*import...*/
public class Main {
    /**
     * SVG画像に図を描画しファイルに書き出す
     * @param args
     */
    public static void main(String[] args) {
        // SVGGraphics2Dオブジェクトを用意する
        SVGGraphics2D g = new SVGGraphics2D(320, 240);
        // SVGGraphics2Dオブジェクトに図を描画する
        exampleDrawAndFill(g);
        examplePath(g);
        exampleArea(g);
        exampleTransform(g);
        // SVGGraphics2DオブジェクトからSVG画像の文字列を取得しファイルに書き出す
        String svg = g.getSVGDocument();
        try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) {
            writer.print(svg);
        } catch (FileNotFoundException exp) {
            exp.printStackTrace();
        }
    }
    public static void exampleDrawAndFill(Graphics2D g) {/*...*/}
    public static void examplePath(Graphics2D g) {/*...*/}
    public static void exampleArea(Graphics2D g) {/*...*/}
    public static void exampleTransform(Graphics2D g) {/*...*/}
}

実行結果

example2ディレクトリにおいて,./gradlew runを実行して書き出されたSVG画像(example2/example.svg)を以下に示します.

JavaFXウィンドウとSVG画像への同じ図の描画

JavaFXウィンドウとSVG画像への同じ図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
    // JavaFXを利用するためにOpenJFXのプラグインを追加する
    id 'org.openjfx.javafxplugin' version '0.0.8'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// JavaFXに関する設定をする
javafx {
    // 使用するJavaFXのバージョンを指定する
    version = "13.0.1"
    // JavaFXで使用するモジュールを指定する
    modules = ['javafx.controls', 'javafx.swing']
}
// example3.Mainクラスをメインクラスとして指定する
mainClassName = 'example3.Main'
// FXGraphics2DライブラリとJFreeSVGライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8'
    implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4'
}

Main.java

package example3;
/*import...*/
public class Main {
    /**
     * JavaFXウィンドウ上にドラッグ軌跡を描画するPNG画像に書き出す
     * JavaFXウィンドウに描画されたものと同じドラッグ軌跡をSVG画像にも書き出す
     */
    public static void main(String[] args) {
        // Appクラスを指定してアプリケーションを起動する
        Application.launch(App.class, args);
    }
    public static class App extends Application {
        /** 直前のドラッグ位置 */
        Point2D previousPoint;
        /**
         * アプリケーション起動時に呼び出される
         * @param primaryStage
         */
        @Override
        public void start(Stage primaryStage) {
            int width = 640;
            int height = 480;
            // Canvasオブジェクトを生成する
            Canvas canvas = new Canvas(width, height);
            // FXGraphics2Dオブジェクトを生成する
            FXGraphics2D fxGraphics2D = new FXGraphics2D(canvas.getGraphicsContext2D());
            // SVGGraphics2Dオブジェクトを生成する
            SVGGraphics2D svgGraphics2D = new SVGGraphics2D(width, height);
            // FXGraphics2DオブジェクトとSVGGraphics2Dオブジェクトをまとめたリストを生成する
            List<Graphics2D> graphics2DList = Arrays.asList(fxGraphics2D, svgGraphics2D);
            // Canvasオブジェクトにマウスプレス時のイベントハンドラを設定する
            canvas.setOnMousePressed(e -> {
                // マウスプレス時には図をクリアして現在のカーソル位置を保存する
                graphics2DList.forEach(g -> {
                    g.setBackground(Color.WHITE);
                    g.clearRect(0, 0, width, height);
                });
                previousPoint = new Point2D.Double(e.getX(), e.getY());
            });
            // Canvasオブジェクトにマウスドラッグ時のイベントハンドラを設定する
            canvas.setOnMouseDragged(e -> {
                // マウスドラッグ時には直前のカーソル位置と現在のカーソル位置の間に線分を描画して現在のカーソル位置を保存する
                Point2D.Double currentPoint = new Point2D.Double(e.getX(), e.getY());
                graphics2DList.forEach(g -> g.draw(new Line2D.Double(previousPoint, currentPoint)));
                previousPoint = currentPoint;
            });
            // Canvasオブジェクトにマウスリリース時のイベントハンドラを設定する
            canvas.setOnMouseReleased(e -> {
                // マウスリリース時にはSVG画像とPNG画像の書き出しを行う
                // PNG画像を書き出す
                SnapshotParameters params = new SnapshotParameters();
                WritableImage img = canvas.snapshot(params, new WritableImage(width, height));
                try {
                    File file = new File("example.png");
                    ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                // SVG画像を書き出す
                String svg = svgGraphics2D.getSVGDocument();
                try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) {
                    writer.print(svg);
                } catch (FileNotFoundException exp) {
                    exp.printStackTrace();
                }
            });
            // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する
            primaryStage.setScene(new Scene(new Pane(canvas)));
            primaryStage.show();
        }
    }
}

実行結果

example3ディレクトリにおいて,./gradlew runを実行しました. 表示されたウィンドウにドラッグ軌跡が描画される様子を以下に示します.

書き出された(example3/example.png)を以下に示します. f:id:Jumpaku:20191213233934p:plain 書き出されたSVG画像(example3/example.svg)を以下に示します.

まとめ

  • Graphic2Dクラスの基本的な使い方として,基本図形のストローク描画とフィル描画,パス操作,領域操作,アフィン変換を示した.
  • JavaFXのウィンドウやSVG画像に図を描画する例を示した.
  • FXGraphics2DやSVGGraphics2Dを利用しGraphic2Dクラスを通して統一的なプログラムで図の描画を行う例を示した.
  • Java13で,JavaFX,FXGraphics2D,JFreeSVGといったライブラリを利用するGradleプロジェクトの設定例を示した.
  • JavaFXJDKに同梱されなくなって,準備が面倒だと思った.