Reactで新幹線風電光掲示板を作ってみました
Reactの練習のために、何か作ってみたいという思いはあったのですが、Reactで試すのに適した課題を見付けられずにいました。 新しいフレームワークを試す時の定番であるWebのTODOアプリを作ってみることも考えたのですが、Webアプリだと「SPA(Single Page Application)じゃなくてもできるよね」という印象になってしまう気がして手が止まっていました。
簡単なデスクトップアプリをReactを使って作ってみるのがいいんじゃないかと考えて電卓を作ってみたりした流れから、新幹線風電光掲示板を作ってみることにしました。ReactもTypeScriptも慣れないので、使い方を間違ってるところ等ありましたら、教えてください。
サンプル
buildしたものを、S3にアップロードしました。
http://smeghead-react-practice.s3-website-ap-northeast-1.amazonaws.com/
ソースコード
- App.tsx
最初は、アルファベットのフォントの配置情報を自前で定義しておいて、それを元に表示しようとしていましたが、BDFフォントというフォントは、テキストで定義されたフォントであり、パースも簡単そうだったので、8×8 ドット日本語フォント「美咲フォント」 を使わせてもらうことにしました。
ということで、Reactの練習と言いつつもApp.tsxの半分くらいは、 BDFフォントのパース処理になってしまいました。
Boardコンポーネントへ、props経由で 電光掲示板に表示する文字列(displayString)を渡しています。
import './App.css'; import React, {useState} from 'react' import Board from './board/board' import Letter from './board/letter' const init = async function() { let bdf = ''; await fetch('misaki_gothic.bdf') .then(response => response.text()) .then(data => { bdf = data; }) const font: any = {} let letter = new Letter(0, [], [], []) let bitmapLines = false bdf.split(/\n/).forEach(line => { const columns = line.split(/\s+/); switch (columns[0]) { case 'STARTCHAR': bitmapLines = false; break case 'ENCODING': letter.encoding = Number(columns[1]) break case 'DWIDTH': letter.dwidth = [Number(columns[1]), Number(columns[1])] break case 'BBX': letter.bbx = [Number(columns[1]), Number(columns[2]), Number(columns[3]), Number(columns[4])] break case 'BITMAP': bitmapLines = true break case 'ENDCHAR': font[letter.encoding.toString()] = letter letter = new Letter(0, [], [], []) break default: if (bitmapLines) { letter.bitmap.push(columns[0]) } } }) return font } let font = {} init().then(f => { font = f }); const App = () => { const [displayString, setDisplayString] = useState(''); return ( <div className="App"> <h2>新幹線の電光掲示板風</h2> <p>Reactの練習のために作りました。</p> <input type="text" onChange={e => setDisplayString(e.target.value)} placeholder='文字を入力してください。'/> <Board str={displayString} font={font} /> </div> ); } export default App; |
- letter.ts
フォントの情報を格納するために用意したクラスです。
class Letter { encoding: number; dwidth: number[]; bbx: number[]; bitmap: string[]; constructor(encoding: number, dwidth: number[], bbx: number[], bitmap: string[]) { this.encoding = encoding this.dwidth = dwidth this.bbx = bbx this.bitmap = bitmap } getBuffer() { const buffer = ['', '', '', '', '', '', '', '', '', ''] let i = 0 for (; i < this.bbx[3] + 2; i++) { buffer[i] += this.dwidth[0] === 4 ? '0000' : '00000000'; } this.bitmap.concat().reverse().forEach(dex => { buffer[i++] += parseInt(dex, 16).toString(2).padStart(8, '0').substring(0, this.dwidth[0]) }) for (; i < 10; i++) { buffer[i] += this.dwidth[0] === 4 ? '0000' : '00000000'; } return buffer; } } export default Letter; |
- board.tsx
電光掲示板コンポーネントです。 propsのdisplayStringで受け取った文字列を、フォントのビットマップ情報を元に、0/1の配列に変換してます。 実際の表示をする子コンポーネントのLineコンポーネントに、props経由で点1行毎に渡しています。(10行で1文字を構成している)
文字が流れるようにするための仕組みは、useEffectによりsetTimeoutで 50ミリ秒後にoffsetを更新することで実現しています。
import React, {useState, useEffect } from 'react' import Line from './line' import Letter from './letter' const boardStyle = { margin: '5px auto', width: 500, height: 100, backgroundColor: 'black', }; const generateBuffer = (str: string, font: {[name: string]: Letter}) => { const buffer = ['', '', '', '', '', '', '', '', '', ''] if (!font) { return buffer; } str += ' '; //文と文の繰り返し時の隙間を作る str.split('').forEach(s => { const charCode: string = s.charCodeAt(0).toString() if (!(font[charCode])) { console.log('no kye', charCode, font[charCode]) return } const letter = font[charCode] letter.getBuffer().forEach((line: string, i: number) => { buffer[i] += line }) }) return buffer; }; const Board = (props: {str: string, font: {[name: string]: Letter}}) => { const [buffer, setBuffer] = useState(generateBuffer(props.str, props.font)) const [offset, setOffset] = useState(50) useEffect(() => { setBuffer(generateBuffer(props.str, props.font)) }, [props]); useEffect(() => { if (Object.keys(props.font).length === 0) { return; } const timerId = setTimeout(() => { setOffset(offset - 1) }, 50) return () => clearTimeout(timerId) }, [offset, props.font]) return ( <div className="Board" style={boardStyle}> {[...Array(10).keys()].reverse().map(i => <Line key={i} buffer={buffer[i]} offset={offset} />)} </div> ); } export default Board; |
- line.tsx
掲示板の点の1行を表すコンポーネントです。 設定された文字がループで表示され続けるようにしています。
import React, { useState, useEffect } from 'react' const dotStyle = { float: 'left', width: '8px', height: '8px', margin: '1px', borderRadius: '5px', background: '#333', } const Line = (props: { buffer: string, offset: number }) => { const [buffer, setBuffer] = useState<string>('') useEffect(() => { setBuffer(props.buffer) }, [props]); const lightning = (x: number, offset: number) => { if (!buffer) { return '#333'; } let virtualScreenBuffer = '' if (offset >= 0) { virtualScreenBuffer = '0'.repeat(offset) + buffer } else { virtualScreenBuffer = buffer.substring((offset % buffer.length) * -1) } // 繰り返し while (virtualScreenBuffer.length < 50) { virtualScreenBuffer += buffer } // console.log(virtualScreenBuffer) if (virtualScreenBuffer.substring(x, x + 1) === '1') { return 'radial-gradient(farthest-corner at 8px 8px, #ffffff 0, #fa8916 70%, #f87d02 100%)'; } return '#333'; }; return ( <div className="Line"> {[...Array(50).keys()].map(i => <div key={i} className="dot-off" style={{ ...dotStyle, background: lightning(i, props.offset) }} />)} </div> ); } export default Line; |
githubは以下です。
https://github.com/smeghead/react-board
助言等おねがいします。