LoginSignup
10

More than 5 years have passed since last update.

Reactjsを使ったLive Codingもどき!

Last updated at Posted at 2015-01-04

メモ取りながらは大変ですねー。

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三昧でした!!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10