メモ取りながらは大変ですねー。
1/4 15:30 妄想
React.js面白いですねー。
っと、写経も飽きたのでそろそろ何か作りたいです。
でも明日から仕事なので今日までに!!
私はゲームエンジン作りたいという野望があるのですが、さすがに大きすぎです!
なので、当たり判定エディタもどきを作りましょう。
このアプリのフローはこんな感じでしょうか!!
- 画像を読み込む
- マウスで連続して直線を引く
- 直線をクローズして、当たり判定に変換
- 頂点をドラッグで動かす
さて、フロントサイドJavaScriptの知識はほとんどないんですが、はたして作れるのでしょうか!?
1/4 15:46 環境構築
- webpack
- CoffeeScript
- React.js
- react-art
を使います!もりもり構築します〜
npm install -save react react-art webpack coffee-script coffee-loader
はい、ではwebpack.config.jsを書きます!
module.exports = {
devtool: "source-map",
entry: {
app: ['webpack/hot/dev-server', './app/index.js']
},
output: {
path: './build',
filename: 'bundle.js'
},
devServer: {
contentBase: "./build"
},
module: {
loaders: [
{ test: /\.coffee$/, loader: "coffee-loader" }
]
},
resolve: {
extensions: ["", ".web.coffee", ".web.js", ".coffee", ".js"]
}
};
とりあえずこれで!
app/index.jsに
console.log("HOGE");
build/index.htmlに
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="bundle.js"></script>
</head>
<body>
</body>
</html>
webpack-dev-server --hot
でlocalhost:8080にアクセス…見事HOGEがでました!!OKっぽいです!
1/4 15:58 dnd受け付け
http://www.html5rocks.com/ja/tutorials/file/dndfiles/
を読んでみます。ふむふむ。
もさっと組んでみます。API全然把握してなくて、DOM操作もままならないです……
window.onload = ->
target = document.getElementById 'target'
target.ondragover = (ev)->
ev.stopPropagation()
ev.preventDefault()
target.ondrop = (ev)->
ev.stopPropagation()
ev.preventDefault()
file = ev.dataTransfer.files[0]
if file?
reader = new FileReader()
reader.onload = (fileEvent)->
img = new Image()
img.src = fileEvent.target.result
target.appendChild img
reader.readAsDataURL file
index.htmlを以下のように書き換え。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="bundle.js"></script>
<style>
html,body,div{
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="target"></div>
</body>
</html>
動きました!!ではそろそろReact.jsにはいります!!
1/4 16:22 コンポーネントを考える
構成物を想像します。作りたい物を絵に描いて、操作を想像します。
- Workspace
- -Image
- -HitArea
- --Vertex
こんな階層構造になりそうですね。たぶん!
1/4 16:25 Workspaceの構築
数日前にreact-caffeineというマークアップラッパを作ったので、それをつかいます!
# workspace.coffee
React = require 'react'
caffeine = require 'react-caffeine'
require './hit-area'
require './image'
caffeine.register
Workspace:
handleDragOver: (ev)->
ev.stopPropagation()
ev.preventDefault()
handleDrop: (ev)->
ev.stopPropagation()
ev.preventDefault()
file = ev.dataTransfer.files[0]
if file?
reader = new FileReader()
reader.onload = (fileEvent)=>
@setState src: fileEvent.target.result
reader.readAsDataURL file
getInitialState: ()->
{src: null}
render: ->
caffeine @, ($)->
@div
onDrop: $.handleDrop
onDragOver: $.handleDragOver
style:
width: @props.width
height: @props.height
, ->
@Image src: $.state.src, width: @props.width, height: @props.height
@HitArea hitArea for hitArea in @props.hitAreaList
# main.coffee
React = require 'react'
art = require 'react-art'
caffeine = require 'react-caffeine'
require './workspace'
caffeine.registerClass art
hitArea = []
window.onload = ->
target = document.getElementById 'target'
React.render (caffeine ->
@Workspace
width: target.clientWidth
height: target.clientHeight
hitAreaList: hitArea
), target
# image.coffee
React = require 'react'
art = require 'react-art'
caffeine = require 'react-caffeine'
caffeine.register
Image:
handleOnLoad: (ev)->
img = @getDOMNode()
cx = @props.width / 2
cy = @props.height / 2
dx = img.width / 2
dy = img.height / 2
@setState left: cx - dx, top: cy - dy
getInitialState: ()->
{left: 0, top: 0}
render: ->
caffeine @, ($)->
@img
onLoad: $.handleOnLoad
src: @props.src
style:
position: 'absolute'
left: $.state.left
top: $.state.top
# hit-area.coffee
React = require 'react'
art = require 'react-art'
caffeine = require 'react-caffeine'
caffeine.register
HitArea:
render: ->
caffeine @, ($)->
@div
これでdndしたイメージが中央に表示されるようになりました!!
1/4 17:05 操作を考える
どこでもいいので、クリックしたらライン作成をスタート、そして終点付近をクリックしたら、パスを閉じる、という感じにしましょう。
1/4 17:10 ライン描画の実装
hitAreaにポイントをプロットするLineToolクラスを作成です。
# line-tool.coffee
CLOSE_DISTANCE = 10
class LineTool
constructor: ()->
@current = @newHitArea()
clear: ()->
@current = @newHitArea()
newHitArea: ()->
points: []
closed: false
addPoint: (origin, ev)->
@current.points.push @computeDelta origin, ev
modifyPoint: (origin, ev)->
@current.points[@current.points.length-1] = @computeDelta origin, ev
computeDelta: (b, ev)->
{x: ev.clientX - b.x, y: ev.clientY - b.y}
nearStartPoint: (p)->
if @current.points[2]?
b = @current.points[0]
dx = p.x - b.x
dy = p.y - b.y
dx*dx+dy*dy < CLOSE_DISTANCE*CLOSE_DISTANCE
hitArea: ()->
@current
move: (origin, ev)->
if !@current.closed and @current.points[0]?
@modifyPoint origin, ev
return true
return false
up: (origin, ev)->
@modifyPoint origin, ev
p = @computeDelta origin, ev
if @nearStartPoint p
@current.closed = true
@current.points.pop()
else
@addPoint origin, ev
down: (origin, ev)->
if @current.points.length == 0
@addPoint origin, ev
module.exports = LineTool
雑ですがだいたいこんな感じになりました!!
1/4 18:10 再開、レンダリング
ちょっとめんどうな所なのですが、イメージの大きさはImageComponentが計算しています。よってImageComponentからイメージの大きさを聞き出さないといけません。これにはreact.jsのrefsの機能を使います。
親コンポーネントが問い合わせて、子コンポーネントに伝播する仕組みです。
1/4 18:50 いったん完成!!
考慮外の操作をするとへんなことが起きますが、一応完成しました!ばんざい!!ここで一旦コミットして、付加機能を考えます!!
1/4 18:50 頂点の移動機能を考える
次は頂点の移動です。
頂点の移動処理自体はそこまで難しくはないでしょうが、問題は実装箇所です。Workspaceのハンドラを切り替えないといけません!ですが、現状どうなってるでしょうかね……
# workspace.coffee
React = require 'react'
caffeine = require 'react-caffeine'
LineTool = require './line-tool'
require './hit-area'
require './image'
caffeine.register
Workspace:
handleDragOver: (ev)->
ev.stopPropagation()
ev.preventDefault()
handleMouseUp: (ev)->
tool = @state.tool
tool.up @origin(), ev
if tool.hitArea().closed
tool.clear()
@setState editing: false
handleMouseDown: (ev)->
tool = @state.tool
tool.down @origin(), ev
unless @state.editing
@props.hitAreaList.push tool.hitArea()
@setState editing: true
@forceUpdate()
handleMouseMove: (ev)->
tool = @state.tool
@forceUpdate() if tool.move @origin(), ev
origin: ()->
s = @refs.image.state
{x: s.left, y: s.top}
handleDrop: (ev)->
ev.stopPropagation()
ev.preventDefault()
file = ev.dataTransfer.files[0]
if file?
reader = new FileReader()
reader.onload = (fileEvent)=>
@setState src: fileEvent.target.result
reader.readAsDataURL file
getInitialState: ()->
{src: null, tool: new LineTool(), editing: false}
render: ->
tool = @state.tool
caffeine @, ($)->
@div
onDrop: $.handleDrop
onDragOver: $.handleDragOver
onMouseDown: $.handleMouseDown
onMouseUp: $.handleMouseUp
onMouseMove: $.handleMouseMove
style:
width: @props.width
height: @props.height
, ->
@Image
src: $.state.src
width: @props.width
height: @props.height
ref: 'image'
@Surface
style:
position: 'absolute'
width: @props.width
height: @props.height
, ->
for hitArea in @props.hitAreaList
@HitArea
hitArea: hitArea
origin: $.origin()
わお!ハンドラ部の責務分割が微妙におかしいです!!
これだと切り替えるにもきりかえられません!!
まずはハンドラをリファクタリングですねー。
見てみると、editingというステートが悪さをしているようですので、これをToolの中に押し込めます。
handleMouseUp: (ev)->
@forceUpdate() if @state.tool.up @origin(), ev
handleMouseDown: (ev)->
@forceUpdate() if @state.tool.down @origin(), ev
handleMouseMove: (ev)->
@forceUpdate() if @state.tool.move @origin(), ev
ハンドラ部はこうなりました、すっきりです!!ここで一旦コミットです。
1/4 19:06 頂点編集ツールの作成
では改めて頂点ツールを作りましょう!こんな感じでしょうか!!
# vertex-tool.coffee
BaseTool = require './tool'
class VertexTool extends BaseTool
constructor: (args)->
@target = args.target
@hitArea = args.hitArea
modifyVertex: (origin, ev)->
p = @computeDelta origin, ev
@hitArea.points[@target] = p
up: (origin, ev)->
@modifyVertex origin, ev
true
move: (origin, ev)->
@modifyVertex origin, ev
true
down: ()->
true
1/4 19:14 ツールの切り替え処理
さて、ツール切り替えのタイミングは、頂点でマウスが押されたタイミングです。これを知ってるのはHitAreaですね。
して復帰タイミングは、マウスが上がったタイミングです!これを知ってるのはWorkspaceです。
こまりました!いろんなところに散らばってます。
なので、Toolをまとめあげる、ToolBoxクラスを作成しましょう。
Tools =
vertex: require './vertex-tool'
line: require './line-tool'
class ToolBox
constructor: ()->
@toolStack = []
@pushTool('line')
isBusy: ()->
@currentTool().isBusy()
currentTool: ()->
@toolStack[@toolStack.length-1]
popTool: ()->
@toolStack.pop()
pushTool: (name, args)->
args = args || {}
args.toolBox = @
console.log args
@toolStack.push new Tools[name](args)
setHitAreaList: (list)->
tool.setHitAreaList?(list) for tool in @toolStack
null
module.exports = ToolBox
これを使うように各々修正します。
一応想定された機能は作り終えました!
つくったものはここへー
https://github.com/necoco/react-sample-stage-editor
思った事
Virtual DOM強い!!
強いですねー。DOM操作を差分でしなくていいのはとても楽です。
Component間通信はpropsで行う
親から子はすんなりいきますが、子から親への通信は、propsに関数なりオブジェクトなりを渡して、コールバックを行う必要があります。
以上です!!今年初めはReact.js三昧でした!!