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-appv2.0.xまでのやり方で、v2.1からはcreate-react-app --typescriptという形で公式サポートされるようになります。ただし、こちらはts-loaderではなくbabelによるトランスパイルのようなので、多少動作に違いが出るかもしれません。 ↩



