TRPGをやったりしてる人は計算ツールがほしくなりますよね。
いちいち本を開いて数字を見て……ってやってるとまどろっこしいです。
というわけで、今回は例として、用紙サイズとDPIを入れるとピクセル数を計算してくれるツールをReact + TypeScriptで作ります。jQueryなんて投げ捨てよう!
環境構築
まずは node
と npm
を入れておいてください。ターミナルかコマンドプロンプト開いて node -v
と npm -v
って叩いてバージョンが出ればOKです。 yarn
使うかどうかはお好みで。
次に create-react-app
を入れます。
> npm i -g create-react-app
プロジェクトのフォルダを置きたいディレクトリの中に cd
で移動します。
例えば、 /home/hibikine/src/calc-tool
に作りたければ、 cd /home/hibikine/src
と移動してください。
/
> cd /home/hibikine/src
/home/hibikine/src
>
プロジェクトを作成します。今回は calc-tool
という名前で、TypeScript使って作ります。1
> create-react-app calc-tool --scripts-version=react-scripts-ts
... いろいろ出る
Initialized a git repository.
Success! Created calc-tool at /mnt/c/Users/goods/src/calc-tool
Inside that directory, you can run several commands:
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
yarn test
Starts the test runner.
yarn eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd calc-tool
yarn start
Happy hacking!
生成されたディレクトリ内に移動し、 npm run start
( yarn
の人は yarn start
)します。
/home/hibikine/src
> cd calc-tool
/home/hibikine/src/calc-tool
> yarn start
http://localhost:3000 にアクセスし、次のような画面が出ればOKです。
実装
生成直後のフォルダ構成を見ると次のようになっています。
node_modules/
public/
src/
App.css
App.test.tsx
App.tsx
index.css
index.tsx
logo.svg
registerServiceWorker.ts
.gitignore
... etc
App.tsx
を開き、中身を次のように書き換えて保存します。
import * as React from 'react';
import './App.css';
interface IState {
paperSize: number;
dpi: number;
}
class App extends React.Component {
public state = { paperSize: 0, dpi: 0 };
public render() {
const { paperSize, dpi } = this.state;
return (
<div className="App">
<header className="App-header">
<h1 className="App-title">ピクセル数計算機</h1>
</header>
<p className="App-main">
<table>
<tbody>
<tr>
<td>
<label htmlFor="paperSize">A</label>
</td>
<td>
<input id="paperSize" type="number" value={paperSize} />
</td>
</tr>
<tr>
<td>
<label htmlFor="dpi">DPI</label>
</td>
<td>
<input id="dpi" type="number" value={dpi} />
</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td>
<label htmlFor="width-mm">幅(mm)</label>
</td>
<td>
<input id="width-mm" readOnly={true} value={5} />
</td>
</tr>
<tr>
<td>
<label htmlFor="height-mm">高さ(mm)</label>
</td>
<td>
<input id="height-mm" readOnly={true} value={5} />
</td>
</tr>
<tr>
<td>
<label htmlFor="width-px">幅(px)</label>
</td>
<td>
<input id="width-px" readOnly={true} value={5} />
</td>
</tr>
<tr>
<td>
<label htmlFor="height-px">高さ(px)</label>
</td>
<td>
<input id="height-px" readOnly={true} value={5} />
</td>
</tr>
</tbody>
</table>
</p>
</div>
);
}
}
export default App;
ついでに App.css
も書き換えます。
.App {
text-align: center;
}
.App-header {
background-color: #222;
height: 80px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-main {
display: flex;
flex-direction: column;
align-items: center;
}
ファイルを保存すると画面が自動で更新されて次のようになります。
この段階だと数字が変更できないので修正していきましょう。
数字を変えられるようにする
class App
の末尾に onChange
メソッドを追加しましょう。
private onChange = (key: keyof IState) => (
e: React.FormEvent<HTMLInputElement>
) => this.setState({ [key]: parseInt(e.currentTarget.value, 10) } as any);
これをそれぞれの input
に設定し、 input
の中身が書き換えられたときに App
内の state
が書き換わるようにします。
state
の書き換えは必ず setState
メソッドを通して行うようにします。
<tr>
<td>
<label htmlFor="paperSize">A</label>
</td>
<td>
<input
id="paperSize"
type="number"
value={paperSize}
onChange={this.onChange('paperSize')}
/>
</td>
</tr>
<tr>
<td>
<label htmlFor="dpi">DPI</label>
</td>
<td>
<input
id="dpi"
type="number"
value={dpi}
onChange={this.onChange('dpi')}
/>
</td>
</tr>
これで数字が書き換えられるようになります。
計算
最後に出力が書き換わるようにします。
class App
の上に getWidth
メソッドと calcPixel
メソッドを追加しましょう。A系の紙のサイズは再帰で求められます。
interface IState {
paperSize: number;
dpi: number;
}
const getWidth = (n: number): number => {
if (n <= 0) {
return 1189;
} else if (n === 1) {
return 841;
}
return Math.floor(getWidth(n - 2) / 2);
};
const calcPixel = (len: number, dpi: number) => Math.floor((len / 25.4) * dpi);
class App extends React.Component<{}, IState> {
render
メソッドを書き換えます。
public render() {
const { paperSize, dpi } = this.state;
const width = getWidth(paperSize);
const height = getWidth(paperSize + 1);
const pixelWidth = calcPixel(width, dpi);
const pixelHeight = calcPixel(height, dpi);
return (
<div className="App">
<header className="App-header">
<h1 className="App-title">ピクセル数計算機</h1>
</header>
<p className="App-main">
<table>
<tbody>
<tr>
<td>
<label htmlFor="paperSize">A</label>
</td>
<td>
<input
id="paperSize"
type="number"
value={paperSize}
onChange={this.onChange('paperSize')}
/>
</td>
</tr>
<tr>
<td>
<label htmlFor="dpi">DPI</label>
</td>
<td>
<input
id="dpi"
type="number"
value={dpi}
onChange={this.onChange('dpi')}
/>
</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td>
<label htmlFor="width-mm">幅(mm)</label>
</td>
<td>
<input id="width-mm" readOnly={true} value={width} />
</td>
</tr>
<tr>
<td>
<label htmlFor="height-mm">高さ(mm)</label>
</td>
<td>
<input id="height-mm" readOnly={true} value={height} />
</td>
</tr>
<tr>
<td>
<label htmlFor="width-px">幅(px)</label>
</td>
<td>
<input id="width-px" readOnly={true} value={pixelWidth} />
</td>
</tr>
<tr>
<td>
<label htmlFor="height-px">高さ(px)</label>
</td>
<td>
<input id="height-px" readOnly={true} value={pixelHeight} />
</td>
</tr>
</tbody>
</table>
</p>
</div>
);
}
正しく計算が行われればOKです!
デプロイする
GitHub Pagesに投げます。 yuitnnn さんの「GitHub PagesにReactアプリをデプロイする方法」を参考にしました!
まず gh-pages
を入れます。npmの人は npm i gh-pages --save-dev
で。
> yarn add --dev gh-pages
次に package.json
に homepage
と predeploy
と deploy
を追記します。
homepageの先頭部分は自分のGitHubのIDにしたり、後ろはリポジトリ名にしたりしてください。
"homepage": "https://hibikinekage.github.io/calc-tool",
"scripts": {
// ...,
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
GitHubにリポジトリを作成し、リモートに登録してデプロイします。URLは適宜自分のものに変えてください。
> git init
> git remote add origin https://github.com/HibikineKage/calc-tool
> yarn deploy # npm run deploy
自分のIDの https://hibikinekage.github.io/calc-tool で見れるようになります。これで終了です。
まとめ
create-react-app
使うと簡単に色々作れて楽しいです! ちょっとした小物をポンポン作りたい時もjQueryよりもおすすめします!
ソースコード: GitHub
最後に全体のコードを掲載しておきます。
import * as React from 'react';
import './App.css';
interface IState {
paperSize: number;
dpi: number;
}
const getWidth = (n: number): number => {
if (n <= 0) {
return 1189;
} else if (n === 1) {
return 841;
}
return Math.floor(getWidth(n - 2) / 2);
};
const calcPixel = (len: number, dpi: number) => Math.floor((len / 25.4) * dpi);
class App extends React.Component<{}, IState> {
public state = { paperSize: 0, dpi: 0 };
public render() {
const { paperSize, dpi } = this.state;
const width = getWidth(paperSize);
const height = getWidth(paperSize + 1);
const pixelWidth = calcPixel(width, dpi);
const pixelHeight = calcPixel(height, dpi);
return (
<div className="App">
<header className="App-header">
<h1 className="App-title">ピクセル数計算機</h1>
</header>
<p className="App-main">
<table>
<tbody>
<tr>
<td>
<label htmlFor="paperSize">A</label>
</td>
<td>
<input
id="paperSize"
type="number"
value={paperSize}
onChange={this.onChange('paperSize')}
/>
</td>
</tr>
<tr>
<td>
<label htmlFor="dpi">DPI</label>
</td>
<td>
<input
id="dpi"
type="number"
value={dpi}
onChange={this.onChange('dpi')}
/>
</td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td>
<label htmlFor="width-mm">幅(mm)</label>
</td>
<td>
<input id="width-mm" readOnly={true} value={width} />
</td>
</tr>
<tr>
<td>
<label htmlFor="height-mm">高さ(mm)</label>
</td>
<td>
<input id="height-mm" readOnly={true} value={height} />
</td>
</tr>
<tr>
<td>
<label htmlFor="width-px">幅(px)</label>
</td>
<td>
<input id="width-px" readOnly={true} value={pixelWidth} />
</td>
</tr>
<tr>
<td>
<label htmlFor="height-px">高さ(px)</label>
</td>
<td>
<input id="height-px" readOnly={true} value={pixelHeight} />
</td>
</tr>
</tbody>
</table>
</p>
</div>
);
}
private onChange = (key: keyof IState) => (
e: React.FormEvent<HTMLInputElement>
) => this.setState({ [key]: parseInt(e.currentTarget.value, 10) } as any);
}
export default App;
-
ちなみに今回の作り方は
create-react-app
v2.0.xまでのやり方で、v2.1からはcreate-react-app --typescript
という形で公式サポートされるようになります。ただし、こちらはts-loaderではなくbabelによるトランスパイルのようなので、多少動作に違いが出るかもしれません。 ↩