TL; DR:これらのエピソードでは、ReactとReduxでSVG要素を制御してゲームを作成する方法を学びます。 このシリーズで得られた知識により、ゲームだけでなくアニメーションを作成できます。 このパートで開発されたソースコードの最終バージョンはGitHubにあります 。
ゲームの名前:「エイリアン、家に帰ろう!」
このシリーズで開発するゲームは、エイリアン、ゲットアウェイホーム! ゲームのアイデアはシンプルです。地球に侵入しようとしている「フライングディスク」を撃ち落とす銃があります。 これらのUFOを破壊するには、マウスにカーソルを合わせてクリックして大砲を発射する必要があります。
興味がある場合は、 ここでゲームの最終バージョンを見つけて実行できます( リンク切れ-翻訳者のコメント )。 しかし、ゲームに参加しないでください、あなたは仕事をしています!
前提条件
記事を読むには、Web開発(主にJavaScriptについて)とNode.jsおよびNPMがプリインストールされているコンピューターについてある程度の知識が必要です。 このチュートリアルシリーズを正常に完了するために、JavaScript、React、Redux、およびSVGの深い知識は必要ありません。 ただし、主題にいる場合は、一部の部分とそれらの相互の対応を理解しやすくなります。
それでも、このシリーズには、注目を集めるに値するトピックをよりよく説明するのに役立つ関連記事、投稿、およびドキュメントへのリンクが含まれています。
始める前に
前のセクションではGitについて言及していませんでしたが、これはいくつかの問題を解決するための優れたツールであることは注目に値します。 すべてのプロの開発者は、Git(またはMercurialやSVNなどの別のバージョン管理システム)を使用して、「ホーム」プロジェクト中でも活動を進めます。
バックアップなしでプロジェクトを作成する理由 あなたもそれを支払う必要はありません。 GitHub (最高!)やBitBucket (悪くない、正直言って)などのサービスを使用して 、信頼できるクラウドインフラストラクチャにコードを保存できます。
このようなツールを使用すると、コードの安全性に自信が持て、開発プロセスを直接促進できます。 たとえば、「バグ」を使用してアプリケーションの新しいバージョンを作成すると、Gitを使用して簡単な手順をいくつか実行するだけで、簡単に以前のバージョンのコードに戻ることができます。
もう1つの重要な利点は、このシリーズの各セクションを追跡し、段階的に開発されたコードをコミットできることです。 これにより、各セクションの後にコードの変更を簡単に確認できます。 このチュートリアルを読みながら、今すぐあなたの人生を楽にしてください。
一般的には、自分でGitをインストールしてください。 また、 GitHubにアカウント(まだ持っていない場合)とプロジェクトを保存するリポジトリを作成します。 次に、各セクションを完了した後、変更をリポジトリーにコミットします。 ああ、 あなたの変更をプッシュすることを忘れないでください 。
Create-React-Appを使用したプロジェクトのクイックスタート
React、Redux、SVGを使用してゲームを作成する最初のステップは、 create-react-app
を使用create-react-app
てプロジェクトをすばやく開始することです。 おそらく既にご存知のとおり(そうでない場合は大したことはありません)、 create-react-appはFacebookがサポートするオープンソースツールであり、開発者がすぐにReactを使い始めるのに役立ちます Node.jsとNPMがインストールされている場合(後者のバージョンは5.2以降である必要があります)、create-react-appをインストールしなくても使用できます。
# npx ( ) # create-react-app npx create-react-app aliens-go-home # cd aliens-go-home
この「ツール」は、次のような構造を作成します。
|- node_modules |- public |- favicon.ico |- index.html |- manifest.json |- src |- App.css |- App.js |- App.test.js |- index.css |- index.js |- logo.svg |- registerServiceWorker.js |- .gitignore |- package.json |- package-lock.json |- README.md
create-react-appツールは人気があり、十分に文書化されており、コミュニティのサポートが良好です。 詳細を調べることに興味がある場合は、githubのcreate-react-appリポジトリを確認し 、ユーザーガイドを読むことができます 。
現時点では、以下にリストされているファイルを取り除くことができます。これは、将来それらが役に立たなくなるためです。
-
App.css
:App
コンポーネントは重要ですが、スタイルは他のコンポーネントで定義されます。 -
App.test.js
:テストは別の記事のトピックかもしれません。 今すぐ使用する必要はありません。 -
logo.svg
:Reactロゴはこのゲームでは使用されません。
これらのファイルを削除すると、プロジェクトを開始しようとするとエラーが発生する場合があります。 これは、。 ./src/App.js
ファイルから2つの「インポート」を削除することで簡単に修正できます。
// ./src/App.js import logo from './logo.svg'; import './App.css';
また、 render()
メソッドをリファクタリングすることにより:
// ... ( ) render() { return ( <div className="App"> <h1>We will create an awesome game with React, Redux, and SVG!</h1> </div> ); } // ... ( - , )
コミットすることを忘れないでください!
ReduxとPropTypesをインストールする
プロジェクトをデプロイし、プロジェクトから不要なファイルを削除した後、アプリケーションで唯一の真のデータソースとして Redux を構成する必要があります。 PropTypesもインストールする必要があります。 これにより、いくつかの一般的なエラーを回避できます。 1つのコマンドで両方のツールをインストールできます。
npm i redux react-redux prop-types
ご覧のとおり、上記のコマンドには3番目のNPMパッケージであるreact-redux
ます。 ReduxをReactで直接使用することはお勧めしません。 react-reduxパッケージは 、パフォーマンスの最適化の面倒な手動処理を行います。
Reduxの構成とPropTypesの使用
説明したパッケージを使用して、Reduxを使用するようにアプリケーションを構成できます。 これは簡単です。 コンテナ (スマートコンポーネント)、 プレゼンテーションコンポーネント(愚かなコンポーネント)、およびレデューサーを作成するだけです。 スマートコンポーネントと愚かなコンポーネントの違いは、最初のコンポーネントが単純に愚かなコンポーネントをReduxに接続することです。 作成する3番目の要素であるレデューサーは、Reduxストアのメインコンポーネントです。 このコンポーネントは、アプリケーションのさまざまなイベントによって引き起こされる「アクション」(アクション)を実行し、これらのアクションに基づいて「ストア」(データソース)を変更する機能を適用します。
このすべてについてわからない場合は、コンポーネント(鈍い)およびコンテナー(スマート)コンポーネントについて詳しく説明しているこの記事を読むことができます。 また、 アクションゲーム 、 レデューサー 、およびストアに慣れるためにRedux実用ガイドを開きます 。 これらの概念を学ぶことを強くお勧めしますが、追加の読書に悩まされることなく勉強を続けることができます。
この要素は他の要素から独立しているため(実際には逆のことが当てはまる)、レデューサーを作成してプロセスを開始する方が便利です。 構造を維持するには、 reducers
という新しいディレクトリを作成し、その中にsrc
を配置し、 index.js
というファイルを追加します。 このファイルには、次のソースコードが含まれる場合があります。
const initialState = { message: `React Redux , ?`, }; function reducer(state = initialState) { return state; } export default reducer;
したがって、Reducerは、ReactとReduxを簡単に統合できるメッセージでアプリケーションの状態を初期化するだけです。 このファイルでは、すぐにアクションの定義と処理を開始します。
その後、 App
コンポーネントをリファクタリングして、このメッセージをユーザーに表示できます。 PropTypes
を使用します。 これを行うには、。 ./src/App.js
ファイルを開き、その内容を次のテキストに置き換えます。
import React, {Component} from 'react'; import PropTypes from 'prop-types'; class App extends Component { render() { return ( <div className="App"> <h1>{this.props.message}</h1> </div> ); } } App.propTypes = { message: PropTypes.string.isRequired, }; export default App;
ご覧のとおり、 PropTypes
を使用すると、コンポーネントが期待する型を決定するのPropTypes
非常に簡単です。 App
コンポーネントのPropTypes
プロパティを必要なパラメーターで設定するだけです。 ネットワークには、基本的なPropTypes
定義と高度なPropTypes
定義を作成する方法を説明したチートシートがあります(たとえば、 これ 、 これ、およびこれ ) 必要に応じてチェックしてください。
store
( store
)の初期状態とApp
コンポーネントの表示内容を決定したら、これらの要素をリンクする必要があります。 これがコンテナの目的です。 構造内にコンテナを作成するには、 src
ディレクトリ内にontainers
という名前のディレクトリを作成する必要があります。 その後、新しいディレクトリで、 Game.js
ファイル内にGame
というコンポーネントを作成します。 このコンテナは、 react-redux
connect
機能を使用して、 state.message
をApp
コンポーネントのメッセージパラメーターに渡します。
import { connect } from 'react-redux'; import App from '../App'; const mapStateToProps = state => ({ message: state.message, }); const Game = connect( mapStateToProps, )(App); export default Game;
最終段階に進みます。 すべてをリンクする最後のステップは、. ./src/index.js
ファイルをリファクタリングしてRedux ストアを初期化し、 Game
コンテナーに転送します(メッセージを受信し、 App
それらを送信( トス )します)。 次のコードは、リファクタリング後の./src/index.js
ファイルの外観を示しています。
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import './index.css'; import Game from './containers/Game'; import reducer from './reducers'; import registerServiceWorker from './registerServiceWorker'; /* eslint-disable no-underscore-dangle */ const store = createStore( reducer, /* preloadedState, */ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), ); /* eslint-enable */ ReactDOM.render( <Provider store={store}> <Game /> </Provider>, document.getElementById('root'), ); registerServiceWorker();
やった! すべてがどのように機能するかを評価するには、ターミナルでプロジェクトのルートに移動してnpm start
を実行します。 したがって、アプリケーションを開発モード( dev-mode )で実行すると、デフォルトのブラウザーで開きます。
ReactでSVGコンポーネントを作成する
このシリーズでは、Reactを使用してSVGコンポーネントを簡単に作成できることに感謝します。 実際、ReactコンポーネントをHTMLとSVGのどちらで作成しても違いはほとんどありません。 主な違いは、新しい要素がSVGに導入され、これらの要素がSVGキャンバスに描画されることです。
SVGとReactを使用して独自のコンポーネントを作成する前に、SVGについて簡単に理解しておくと役立ちます。
SVGの概要
SVGは、最もクールで柔軟なWeb標準の1つです。 SVGは、Scalable Vector Graphicsの略で、開発者が2次元のベクターグラフィックスを記述することができるマークアップ言語です。 SVGはHTMLに非常に似ています。 どちらもXMLベースのマークアップ言語であり、CSSやDOMなどの他のWeb標準とうまく連携します。 CSSルールは、アニメーションを含むSVGとHTMLの両方に等しく適用されます。
このシリーズでは、Reactを使用して、12を超えるSVGコンポーネントを作成します。 ゲームオブジェクト(砲弾を発射する大砲)を作成するには、SVG要素を構成(グループ化)する必要さえあります。
SVGのより詳細な調査は、この記事のフレームワーク内では不可能であり、長すぎます。 SVGマークアップ言語について詳しく知りたい場合は、Mozillaが提供するチュートリアルと 、この記事で紹介するSVG座標系に関する資料をお読み ください 。
ただし、独自のコンポーネントの作成を開始する前に、SVGのいくつかの特性を学ぶことが重要です。 第1に、SVGをDOMと組み合わせて使用すると、開発者は「何かをする」ことができます。 ReactでSVGを使用するのは非常に簡単です。
第二に、SVG座標系はデカルト平面に似ており、逆さまになっています。 したがって、デフォルトでは、負の値はX軸の上に垂直に表示されます。この場合、水平値はデカルト平面と同じように配置されます。つまり、負の値はY軸の左側に配置されます。この動作は、SVGキャンバスに変換を適用することで簡単に変更できます 。 ただし、開発者間の混乱を避けるために、デフォルト設定を使用することをお勧めします。 すぐに慣れるでしょう。
最後に、SVGは多くの新しい要素( circle
、 rect
path
)を導入することに注意してください。 これらの要素を使用するには、HTML要素内で定義するだけでは不十分です。 最初に、すべてのSVGコンポーネントを描画するsvg
要素(キャンバス)を定義する必要があります。
SVG、パス要素、三次ベジェ曲線
SVG要素の描画は、3つの方法で実行できます。 まず、 rect
、 circle
line
などの基本的な要素を使用できます。 ただし、これらの要素は特に柔軟ではありません。 名前(長方形、円、線)に従って単純な図形を描画できます。
2番目の方法は、基本要素を組み合わせて、より複雑な形状を取得することです。 たとえば、家を描くために、同じ辺(正方形を取得)と2本の線で長方形( rect
)を使用できます。 ただし、このアプローチは依然として非常に厳しく制限されています。
3番目の最も柔軟な方法は、 パス要素を使用することです 。 このオプションにより、開発者はかなり複雑なフォームを作成できます。 図を描画するには、ブラウザに特定のコマンドを指定します。 たとえば、「L」を描画するには、3つのコマンドを含むpath
要素を作成できます。
-
M 20 20
:M 20 20
後に定義されたXおよびY座標に「ペン」を移動するブラウザへのコマンド(すなわち20, 20
)。 -
V 80
:ブラウザにコマンドを実行して、Y軸に沿って前のポイントから位置80
に線を引きます。 -
H 50
:前のポイントからX軸に沿って位置50
に線を引くようにブラウザーに指示します。
<svg> <path d="M 20 20 V 80 H 50" stroke="black" stroke-width="2" fill="transparent" /> </svg>
Path
要素は他の多くのコマンドを受け入れます。 最も重要なものの1つは、3次ベジェ曲線のチームです。 2つの基準点と2つの制御点を使用して、「滑らかな」曲線を追加できます。
「各ポイントの3次ベジェ曲線は2つの制御点を取ります。したがって、3次ベジェ曲線を作成するには、3つの座標セットを指定する必要があります。最後の座標セットは、曲線の終了点を示します。他の2つのセットは制御点です。基本的に、コントロールポイントは特定のポイントでのラインの勾配を表します。ベジェ関数は、ラインの先頭に設定した勾配から最後に設定した勾配までの滑らかな曲線を作成します。 -Mozilla Developer Network
たとえば、「U」を描画するには、次の手順を実行します。
<svg> <path d="M 20 20 C 20 110, 110 110, 110 20" stroke="black" fill="transparent"/> </svg>
この場合、 path
要素に渡されるコマンドはブラウザーに次のことを伝えます。
- ポイント
20,20
から描画を開始します。 - 最初の制御点の座標:
20, 110
; - 2番目の制御点の座標:
110, 110
; - 曲線の終点の座標:
110 20
;
3次ベジエ曲線の動作原理をまだ理解していない場合は、絶望しないでください。 このシリーズで練習する機会があります。 さらに、インターネット上でこの機能に関する多くのガイドを見つけることができ、 JSFiddleやCodepenなどのツールでいつでも練習できます。
Canvasコンポーネントの作成
(これは<canvas></canvas>
に関するものではなく、Canvas反応コンポーネント(ロシア語のキャンバス)-翻訳者のコメント)
プロジェクトの構造を作成し、SVGの基本を学習したら、ゲームの作成を開始できます。 作成する最初の要素は、ゲーム要素の描画に使用されるSVGキャンバスです。
このコンポーネントは、プレゼンテーション(愚かな)として特徴付けられます。 したがって、. ./src
ディレクトリ内に./src
ディレクトリを作成して、新しいコンポーネントとその「兄弟」( 隣接/子要素-翻訳者コメント )を保存できます。 これがあなたのキャンバスになるので、 Canvas
よりも自然な名前を思い付くのは難しいです。 ./src/components/
ディレクトリ内に./src/components/
という新しいファイルを作成し、次のコードを追加します。
import React from 'react'; const Canvas = () => { const style = { border: '1px solid black', }; return ( <svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" style={style} > <circle cx={0} cy={0} r={50} /> </svg> ); }; export default Canvas;
Canvas
コンポーネントを使用するようにApp
コンポーネントを書き直します。
import React, {Component} from 'react'; import Canvas from './components/Canvas'; class App extends Component { render() { return ( <Canvas /> ); } } export default App;
プロジェクトを実行し( npm start
)、アプリケーションをテストすると、ブラウザーはこの円の4分の1だけを描画することがわかります。 これは、デフォルトでは、原点が画面の左上隅にあるためです。 さらに、 svg
要素が画面全体を占有していないことがわかります。
より面白くて便利なコントロールを作成するには、キャンバス( <Canvas/>
)を全画面表示に適したものにします。 開始点をX軸の中心に移動して、下部に近づけることができます(後で銃をオリジナルに追加します)。 両方の条件を満たすには、2つのファイルを変更する必要があります: ./src/components/Canvas.jsx
/ ./src/index.css
/ ./src/components/Canvas.jsx
と./src/index.css
。
Canvas
の内容を置き換えることから始めて、次のコードを適用します。
import React from 'react'; const Canvas = () => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( <svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" viewBox={viewBox} > <circle cx={0} cy={0} r={50} /> </svg> ); }; export default Canvas;
このバージョンのコンポーネントでは、 svg
タグのviewBox属性を設定します 。 この属性により、キャンバスとそのコンテンツが特定のコンテナ(この場合、ウィンドウ/ブラウザの内側の領域)に対応する必要があることを決定できます。 ご覧のとおり、viewBox属性は4つの数字で構成されています。
-
min-x
:この値は、ユーザーが見ることができる左端のポイントを定義します。 したがって、画面の中心に表示される軸(および円)を取得するために、画面の幅を「-」記号で2で割って(window.innerWidth / -2
)属性値(min-x
)を取得します。 キャンバスが座標原点の両側に同じ数のポイントを表示するように(-2
)で割る必要があることに注意してください。 -
min-y
:キャンバス上の最高点を定義します。 ここでは、window.innerHeight
の値を100
から減算して、Yの先頭から特定の領域(100
ポイント)を設定する必要があります。 -
width
とheight
:ユーザーが画面に表示するX軸とY軸に沿ったポイントの数を決定します。
viewBox
属性の定義に加えて、新しいバージョンではpreserveAspectRatioという属性も設定します。 キャンバスとその要素の均一なスケーリングを強制するために、 xMaxYMax none
を使用しxMaxYMax none
。
( preserveAspectRatio
インストールすると、reactから警告が発生しました-翻訳者のコメント )
キャンバスをリファクタリングした後、次のルールを./src/index.css
ファイルに追加する必要があります。
/* ... body definition ... */ html, body { overflow: hidden; height: 100%; }
これは、 html
およびbody
要素(タグ)がスクロールを非表示(および無効)にするために行われます。 さらに、アイテムは全画面で表示されます。
今すぐアプリケーションをチェックすると、円は画面の下部の中央の水平方向にあることがわかります。
Skyコンポーネントの作成
キャンバスを全画面解像度に設定し、原点をその中心に配置したら、実際のゲーム要素の作成を開始できます。 ゲームの背景要素である空の設計から始めることができます。 これを行うには、次のコードを使用して./src/components/
ディレクトリにSky.jsx
という新しいファイルを作成します。
import React from 'react'; const Sky = () => { const skyStyle = { fill: '#30abef', }; const skyWidth = 5000; const gameHeight = 1200; return ( <rect style={skyStyle} x={skyWidth / -2} y={100 - gameHeight} width={skyWidth} height={gameHeight} /> ); }; export default Sky;
ゲームにこのような大きな領域が指定されている理由(幅5000
および高さ1200
)を疑問に思うかもしれません。 実際、このゲームの幅は重要ではありません。 画面サイズをカバーするのに十分な数だけ設定する必要があります。
次に、高さが重要です。 ユーザーの画面の解像度と向きに関係なくキャンバス上に1200ポイントを設定するため、これによりゲームの一貫性が確保され、すべてのユーザーに同じ領域が表示されます。 したがって、フライングディスクが表示される場所と、指定されたポイントを通過する時間を決定できます。
キャンバスに空( Sky
コンポーネント)を表示するには、エディターでCanvas.jsx
ファイルを開き、 Canvas.jsx
ように修正します。
import React from 'react'; import Sky from './Sky'; const Canvas = () => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( <svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" viewBox={viewBox} > <Sky /> <circle cx={0} cy={0} r={50} /> </svg> ); }; export default Canvas;
ここでアプリケーションをテストすると( npm start
)、円はまだ下部の中央にあり、背景が青になっていることがわかります。
注 : circle
要素の後にSky
要素を追加すると、 circle
は表示されなくなります。 これは、SVG が z-index
サポートしていないためです。 SVGは、リストされている順序に従って、どの要素が「より高い」かを判別します。 つまり、 Sky
後にcircle
要素を記述して、Webブラウザーが青い背景の上に表示するようにする必要があります。
Groundコンポーネントの作成
Sky
コンポーネントを作成したら、 Ground
コンポーネントの作成に進みます。 これを行うには、。 Ground.jsx
ディレクトリに新しいGround.jsx
ファイルを作成し、次のコードを追加します。
import React from 'react'; const Ground = () => { const groundStyle = { fill: '#59a941', }; const division = { stroke: '#458232', strokeWidth: '3px', }; const groundWidth = 5000; return ( <g id="ground"> <rect id="ground-2" data-name="ground" style={groundStyle} x={groundWidth / -2} y={0} width={groundWidth} height={100} /> <line x1={groundWidth / -2} y1={0} x2={groundWidth / 2} y2={0} style={division} /> </g> ); }; export default Ground;
この要素について素晴らしいことは何もありません。 これは、 rect
要素とline
要素の単なる合成です。 ただし、お気づきのように、この要素には、幅を決定する値5000
定数も含まれています。 したがって、このようなグローバル定数を保存するファイルを作成するとよいでしょう。
この結論に達したので、。 ./src/
ディレクトリ内にutils
という新しいディレクトリを作成し、この新しいディレクトリ内にconstants.js
というファイルを作成します。 現時点では、このファイルに保存する定数は1つだけです。
// , export const skyAndGroundWidth = 5000;
Sky
Ground
, .
Ground
(, (.. Sky
circle
)). - , , .
Cannon ()
, . . , , . . , , .
, : , . , d
path
, : M 20 20 C 20 110, 110 110, 110 20
.
, formula.js
./src/utils/
, :
export const pathFromBezierCurve = (cubicBezierCurve) => { const { initialAxis, initialControlPoint, endingControlPoint, endingAxis, } = cubicBezierCurve; return ` M${initialAxis.x} ${initialAxis.y} c ${initialControlPoint.x} ${initialControlPoint.y} ${endingControlPoint.x} ${endingControlPoint.y} ${endingAxis.x} ${endingAxis.y} `; };
, () ( initialAxis
, initialControlPoint
, endControlPoint
, endAxis
) cubicBezierCurve
, .
, . : CannonBase
() CannonPipe
().
CannonBase
CannonBase.jsx
./src/components
:
import React from 'react'; import { pathFromBezierCurve } from '../utils/formulas'; const CannonBase = (props) => { const cannonBaseStyle = { fill: '#a16012', stroke: '#75450e', strokeWidth: '2px', }; const baseWith = 80; const halfBase = 40; const height = 60; const negativeHeight = height * -1; const cubicBezierCurve = { initialAxis: { x: -halfBase, y: height, }, initialControlPoint: { x: 20, y: negativeHeight, }, endingControlPoint: { x: 60, y: negativeHeight, }, endingAxis: { x: baseWith, y: 0, }, }; return ( <g> <path style={cannonBaseStyle} d={pathFromBezierCurve(cubicBezierCurve)} /> <line x1={-halfBase} y1={height} x2={halfBase} y2={height} style={cannonBaseStyle} /> </g> ); }; export default CannonBase;
. - ( #75450e
) "" - ( #a16012
).
CannonPipe
CannonBase
. , pathFromBezierCurve
, . , transform
.
CannonPipe.jsx
./src/components/
:
import React from 'react'; import PropTypes from 'prop-types'; import { pathFromBezierCurve } from '../utils/formulas'; const CannonPipe = (props) => { const cannonPipeStyle = { fill: '#999', stroke: '#666', strokeWidth: '2px', }; const transform = `rotate(${props.rotation}, 0, 0)`; const muzzleWidth = 40; const halfMuzzle = 20; const height = 100; const yBasis = 70; const cubicBezierCurve = { initialAxis: { x: -halfMuzzle, y: -yBasis, }, initialControlPoint: { x: -40, y: height * 1.7, }, endingControlPoint: { x: 80, y: height * 1.7, }, endingAxis: { x: muzzleWidth, y: 0, }, }; return ( <g transform={transform}> <path style={cannonPipeStyle} d={pathFromBezierCurve(cubicBezierCurve)} /> <line x1={-halfMuzzle} y1={-yBasis} x2={halfMuzzle} y2={-yBasis} style={cannonPipeStyle} /> </g> ); }; CannonPipe.propTypes = { rotation: PropTypes.number.isRequired, }; export default CannonPipe;
CannonBase
CannonPipe
. :
import React from 'react'; import Sky from './Sky'; import Ground from './Ground'; import CannonBase from './CannonBase'; import CannonPipe from './CannonPipe'; const Canvas = () => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( <svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" viewBox={viewBox} > <Sky /> <Ground /> <CannonPipe rotation={45} /> <CannonBase /> </svg> ); }; export default Canvas;
:
! ( Sky
Ground
) ( CannonBase
+ CannonPipe
). . , - , . onmousemove
, .. , , .
, , CannonPipe
. onmousemove
, - () . ( ), ( Redux).
Redux action ( , ) ( — ). , Actions
./src/
. index.js
, :
export const MOVE_OBJECTS = 'MOVE_OBJECTS'; export const moveObjects = mousePosition => ({ type: MOVE_OBJECTS, mousePosition, });
: MOVE_OBJECTS , . .
( index.js
./src/reducers/
):
import { MOVE_OBJECTS } from '../actions'; import moveObjects from './moveObjects'; const initialState = { angle: 45, }; function reducer(state = initialState, action) { switch (action.type) { case MOVE_OBJECTS: return moveObjects(state, action); default: return state; } } export default reducer;
, , MOVE_OBJECTS
, moveObjects
. , , ( initial state ) , angle
45
. .
, moveObjects
. , , , . moveObjects.js
./src/reducers/
:
import { calculateAngle } from '../utils/formulas'; function moveObjects(state, action) { if (!action.mousePosition) return state; const { x, y } = action.mousePosition; const angle = calculateAngle(0, 0, x, y); return { ...state, angle, }; } export default moveObjects;
, x
y
mousePosition
calculateAngle
. , , ( ) .
, , calculateAngle
formula.js
, ? , , , , StackExchange , , . , formula.js
( ./src/utils/formulas
):
export const radiansToDegrees = radians => ((radians * 180) / Math.PI); // https://math.stackexchange.com/questions/714378/find-the-angle-that-creating-with-y-axis-in-degrees export const calculateAngle = (x1, y1, x2, y2) => { if (x2 >= 0 && y2 >= 0) { return 90; } else if (x2 < 0 && y2 >= 0) { return -90; } const dividend = x2 - x1; const divisor = y2 - y1; const quotient = dividend / divisor; return radiansToDegrees(Math.atan(quotient)) * -1; };
: atan
, Math
, . . radiansToDegrees
.
, , . Redux, action ( ) moveObjects
props ( ) App
. Game
. Game.js
( ./src/containers
) :
import { connect } from 'react-redux'; import App from '../App'; import { moveObjects } from '../actions/index'; const mapStateToProps = state => ({ angle: state.angle, }); const mapDispatchToProps = dispatch => ({ moveObjects: (mousePosition) => { dispatch(moveObjects(mousePosition)); }, }); const Game = connect( mapStateToProps, mapDispatchToProps, )(App); export default Game;
( mapStateToProps
mapDispatchToProps
) App
props
. App.js
( ./src/
) :
import React, {Component} from 'react'; import PropTypes from 'prop-types'; import { getCanvasPosition } from './utils/formulas'; import Canvas from './components/Canvas'; class App extends Component { componentDidMount() { const self = this; setInterval(() => { self.props.moveObjects(self.canvasMousePosition); }, 10); } trackMouse(event) { this.canvasMousePosition = getCanvasPosition(event); } render() { return ( <Canvas angle={this.props.angle} trackMouse={event => (this.trackMouse(event))} /> ); } } App.propTypes = { angle: PropTypes.number.isRequired, moveObjects: PropTypes.func.isRequired, }; export default App;
, . :
-
componentDidMount
: ( lifecycle method ) ( setInterval ),moveObjects
; -
trackMouse
:canvasMousePosition
App
.moveObjects
. , HTML-. .canvasMousePosition
. -
render
: ( angle )trackMouse
Canvas
.angle
trackMouse
SVG . -
App.propTypes
: :angle
moveObjects
. , . e, moveObjects — , .
App
formula.js
:
export const getCanvasPosition = (event) => { // mouse position on auto-scaling canvas // https://stackoverflow.com/a/10298843/1232793 const svg = document.getElementById('aliens-go-home-canvas'); const point = svg.createSVGPoint(); point.x = event.clientX; point.y = event.clientY; const { x, y } = point.matrixTransform(svg.getScreenCTM().inverse()); return {x, y}; };
, , StackOverflow .
, — Canvas
. Canvas.jsx
( ./src/components
) :
import React from 'react'; import PropTypes from 'prop-types'; import Sky from './Sky'; import Ground from './Ground'; import CannonBase from './CannonBase'; import CannonPipe from './CannonPipe'; const Canvas = (props) => { const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight]; return ( <svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" onMouseMove={props.trackMouse} viewBox={viewBox} > <Sky /> <Ground /> <CannonPipe rotation={props.angle} /> <CannonBase /> </svg> ); }; Canvas.propTypes = { angle: PropTypes.number.isRequired, trackMouse: PropTypes.func.isRequired, }; export default Canvas;
:
-
CannonPipe.rotation
: . Redux store ( (mapStateToProps mapDispatchToProps) connect, Game — . ). -
svg.onMouseMove
: , . -
Canvas.propTypes
:angle
trackMouse
.
! . npm start
( ). http://localhost:3000/ - . .
, ?!
, . create-react-app
, , . , . , .
, , . .
!
!
翻訳者から
, "" . どう思いますか?