<!DOCTYPE html>
<html>
<head>
  <title>riot-handwrite</title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" type="text/css">
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-material-design/0.5.10/css/bootstrap-material-design.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-material-design/0.5.10/css/ripples.min.css">
  <script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
</head>
<body>
  <div class="container-fluid main">
    <div class="row">
      <div class="col-md-offset-1 col-md-10">
        <my-navbar></my-navbar>
        <div class="row">
          <div class="col-sm-6">
            <my-frame id="input" caption="手書き入力">
              <my-input2></my-input2>
            </my-frame>
          </div>
          <div class="col-sm-6">
            <div class="row">
              <div class="col-sm-12">
                <my-frame id="sum" caption="a + b">
                  <my-sum></my-sum>
                </my-frame>
              </div>
              <div class="col-sm-12">
                <my-frame id="product" caption="a * b">
                  <my-product></my-product>
                </my-frame>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-material-design/0.5.10/js/material.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-material-design/0.5.10/js/ripples.min.js"></script>

<script src="https://rawgit.com/riot/riot/v2.6.7/riot%2Bcompiler.min.js"></script>
<script src="my-navbar.tag" type="riot/tag"></script>
<script src="my-frame.tag" type="riot/tag"></script>
<script src="my-input2.tag" type="riot/tag"></script>
<script src="my-handwrite.tag" type="riot/tag"></script>
<script src="my-sum.tag" type="riot/tag"></script>
<script src="my-product.tag" type="riot/tag"></script>
<script src="obseriot.js"></script>
<script>
var action = obseriot.action;
var store = obseriot.store;

obseriot.defineAction(
  "input", // sotre name
  /* @param {{ a: number, b: number }} nums */
  function(nums) { // handler action
    return nums;
  }
);

obseriot.defineStore(
  "sum",  // sotre name
  function () { // handler action
    return store.sum.state
  },
  0 // default state
);

obseriot.defineStore(
  "product",  // sotre name
  function () { // handler action
    return store.product.state
  },
  0 // default state
);

// アクションのリスナー定義→ストア更新→ストア更新通知
obseriot.listen( action.input, function (nums) {
    store.sum.state = nums.a + nums.b;
    obseriot.notify( store.sum );
});
obseriot.listen(action.input, function (nums) {
    store.product.state = nums.a * nums.b;
    obseriot.notify( store.product );
});

riot.mount('my-navbar');
riot.mount('#input');
riot.mount('my-input');
riot.mount('#sum');
riot.mount('my-sum');
riot.mount('#product');
riot.mount('my-product');

</script>

</body>
</html>
# Riot HandWrite Example

Riotで手書き入力コンポーネントを作った

## Have a play

[Open this example on Plunker](http://plnkr.co/edit/LNgklu?p=preview)

[解説](http://qiita.com/bakenezumi/items/4ea886c28a8bc7bb6ed9)
<my-navbar>
  <div class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-responsive-collapse">
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="javascript:void(0)">Riot BootStrap Material</a>
      </div>
      <div class="navbar-collapse collapse navbar-responsive-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="javascript:void(0)">example</a>
          </li>
        </ul>
      </div>
    </div>
  </div>

  this.on('mount', function() {
    $.material.init();
  })
</my-navbar>
<my-frame>
  <div class="well">
    <h4>{opts.caption}</h4>
    <yield/>
  </div>
  <style>
  body .container-fluid .well {
    padding-top: 8px;
  }
  h4 {
    margin-top: 10px;
    margin-bottom: 10px;
  }
  </style>
</my-frame>
var obseriot = (function () {
  'use strict';

  var observable = function(el) {

    /**
     * Extend the original object or create a new empty one
     * @type { Object }
     */

    el = el || {}

    /**
     * Private variables
     */
    var callbacks = {},
      slice = Array.prototype.slice

    /**
     * Private Methods
     */

    /**
     * Helper function needed to get and loop all the events in a string
     * @param   { String }   e - event string
     * @param   {Function}   fn - callback
     */
    function onEachEvent(e, fn) {
      var es = e.split(' '), l = es.length, i = 0, name, indx
      for (; i < l; i++) {
        name = es[i]
        indx = name.indexOf('.')
        if (name) fn( ~indx ? name.substring(0, indx) : name, i, ~indx ? name.slice(indx + 1) : null)
      }
    }

    /**
     * Public Api
     */

    // extend the el object adding the observable methods
    Object.defineProperties(el, {
      /**
       * Listen to the given space separated list of `events` and
       * execute the `callback` each time an event is triggered.
       * @param  { String } events - events ids
       * @param  { Function } fn - callback function
       * @returns { Object } el
       */
      on: {
        value: function(events, fn) {
          if (typeof fn != 'function')  return el

          onEachEvent(events, function(name, pos, ns) {
            (callbacks[name] = callbacks[name] || []).push(fn)
            fn.typed = pos > 0
            fn.ns = ns
          })

          return el
        },
        enumerable: false,
        writable: false,
        configurable: false
      },

      /**
       * Removes the given space separated list of `events` listeners
       * @param   { String } events - events ids
       * @param   { Function } fn - callback function
       * @returns { Object } el
       */
      off: {
        value: function(events, fn) {
          if (events == '*' && !fn) callbacks = {}
          else {
            onEachEvent(events, function(name, pos, ns) {
              if (fn || ns) {
                var arr = callbacks[name]
                for (var i = 0, cb; cb = arr && arr[i]; ++i) {
                  if (cb == fn || ns && cb.ns == ns) arr.splice(i--, 1)
                }
              } else delete callbacks[name]
            })
          }
          return el
        },
        enumerable: false,
        writable: false,
        configurable: false
      },

      /**
       * Listen to the given space separated list of `events` and
       * execute the `callback` at most once
       * @param   { String } events - events ids
       * @param   { Function } fn - callback function
       * @returns { Object } el
       */
      one: {
        value: function(events, fn) {
          function on() {
            el.off(events, on)
            fn.apply(el, arguments)
          }
          return el.on(events, on)
        },
        enumerable: false,
        writable: false,
        configurable: false
      },

      /**
       * Execute all callback functions that listen to
       * the given space separated list of `events`
       * @param   { String } events - events ids
       * @returns { Object } el
       */
      trigger: {
        value: function(events) {
          var arguments$1 = arguments;


          // getting the arguments
          var arglen = arguments.length - 1,
            args = new Array(arglen),
            fns

          for (var i = 0; i < arglen; i++) {
            args[i] = arguments$1[i + 1] // skip first argument
          }

          onEachEvent(events, function(name, pos, ns) {

            fns = slice.call(callbacks[name] || [], 0)

            for (var i = 0, fn; fn = fns[i]; ++i) {
              if (!ns || fn.ns == ns) fn.apply(el, fn.typed ? [name].concat(args) : args)
              if (fns[i] !== fn) { i-- }
            }

            if (callbacks['*'] && name != '*')
              el.trigger.apply(el, ['*', name].concat(args))

          })

          return el
        },
        enumerable: false,
        writable: false,
        configurable: false
      }
    })

    return el

  }

  var obseriot = new function () {
      observable( this )
  }

  obseriot.listen = function ( e, cb ) {
      if ( ! e.handler ) return
      this.on( e.handler.name, cb )
  }

  obseriot.notify = function ( e ) {
      var arg = [], len = arguments.length - 1;
      while ( len-- > 0 ) arg[ len ] = arguments[ len + 1 ];

      if ( ! e.handler ) return
      var t = [ e.handler.name ],
      f = e.handler.action.apply( this, arg )
      if ( f.constructor.name !== 'Array' ) f = [ f ]
      Array.prototype.push.apply( t, f )
      this.trigger.apply( this, t )
  }

  obseriot.action = {};
  obseriot.defineAction = function(actionName, act) {
    if(obseriot.action[actionName]) throw "action." + actionName +  " id already defined";
    obseriot.action[actionName] = {
      handler : {
        name : "action_" + actionName,
        action : act
      }
    }
  }

  obseriot.store = {};
  obseriot.defineStore = function(storeName, act, defaultState) {
    if(obseriot.store[storeName]) throw "store." + storeName + " id already defined";
    obseriot.store[storeName] = {
      state : defaultState,
      handler : {
        name : "store_" + storeName,
        action : act
      }
    }
  }

  return obseriot;

}());
<my-sum>
  <div class="row">
    <h1 class="col-xs-2">=</h1><h1 class="col-xs-10">{sum}</h1>
  </div>
  sum = store.sum.state;
  var self = this;
  obseriot.listen(store.sum, function() {
    self.sum = store.sum.state;
    self.update();
  })

</my-sum>
<my-product>
  <div class="row">
    <h1 class="col-xs-2">=</h1><h1 class="col-xs-10">{product}</h1>
  </div>
  product = store.product.state
  var self = this
  obseriot.listen(store.product, function() {
    self.product = store.product.state;
    self.update();
  })
</my-product>
<my-input2>
  <div class="row">
    <div class="col-xs-offset-1 col-xs-10">
      <form class="form-horizontal">
        <div class="row">
          <div class="form-group" >
            <label>a</label>
            <my-handwrite name="a" oninput="{notify}"/>
          </div>
        </div>
        <div class="row">
          <div class="form-group" >
            <label>b</label>
            <my-handwrite name="b" oninput="{notify}"/>
          </div>
        </div>
      </form>
    </div>
  </div>
  var self = this
  this.notify = function(e) {
    obseriot.notify(action.input, {
      a: Number(self.tags.a.value),
      b: Number(self.tags.b.value)
    })
  };

</my-input2>
<my-handwrite>
  <div name="container">
    <canvas name="canvas" width={ width } height={ height }
            ontouchstart={ writeStart(touchSwitch) }
            onmousedown={ writeStart(mouseSwitch) }></canvas>
    <label>{ value }</label>
    <button class="btn" onclick={ clear }>
      <!-- Google Material icons https://design.google.com/icons/ を利用(アイコンフォント)
      htmlに右記stylesheetのlinkを付けること <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> -->
      <i>clear</i>
    </button>
    <!-- 一応付けた -->
    <img class="handwrite-powerd" src="https://developers.google.com/places/documentation/images/powered-by-google-on-white.png?hl=ja"></img>
  </div>
  <style>
  /* from https://fonts.googleapis.com/icon?family=Material+Icons */
  @font-face {
    font-family: 'Material Icons';
    font-style: normal;
    font-weight: 400;
    src: local('Material Icons'), local('MaterialIcons-Regular'), url(https://fonts.gstatic.com/s/materialicons/v18/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2) format('woff2');
  }
  my-handwrite i {
    font-family: 'Material Icons';
    font-weight: normal;
    font-style: normal;
    font-size: 24px;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    display: inline-block;
    white-space: nowrap;
    word-wrap: normal;
    direction: ltr;
    -webkit-font-feature-settings: 'liga';
    -webkit-font-smoothing: antialiased;
  }

my-handwrite div {
  position: relative;
  box-sizing: content-box;
  width: 100%;
}
my-handwrite canvas {
  background-color: #f2f2f2;
}
my-handwrite label {
  position: absolute;
  right: 10px;
  bottom: 0px;
}
my-handwrite button.btn {
  position: absolute;
  right:  0px;
  top:    0px;
  margin: 0px;
  padding: 3px;
  width: 30px;
  height: 30px;
  box-shadow: none;
  border: none;
  text-decoration: none;
  background: 0 0;
}
my-handwrite button.btn:active:focus, my-handwrite button.btn:focus {
  outline: none;
}
my-handwrite img {
  position: absolute;
  max-width: 100px;
  height: auto;
  left: 5px;
  bottom: 10px;
}
  </style>

  <script>
var self = this
this.height = opts.height || 100 // 高さは指定可能
this.oninput = opts.oninput  || function(e){} // 認識完了後、もしくはクリア後に呼ぶコールバック
this.strokeStyle = opts.strokeStyle || "#9999FF" // 線の色は指定可能
this.value = '' // 認識した文字を格納

this.width = 0  // 幅はレンダリング後に動的計算(指定不可)
this.callTimer = null // setTimeoutキャンセル用タイマー
this.startTime = 0         // 手書き開始時間
// this.storokes: 手書き内容 [stroke, stroke...]
// ... stroke : タッチ開始から離すまでに書かれた曲線[{x, y, t:手書き開始からの経過時間},...]
this.strokes = []

//touch用描画イベントオンオフスイッチ
this.touchSwitch = {
  on: function(moveHandler, moveEndHandler) {
    $(self.canvas)
      .on('touchmove', function(e) {moveHandler(e.originalEvent.touches[0])})
      .on('touchend', moveEndHandler)
  },
  off: function() { $(self.canvas).off('touchmove').off('touchend') },
}

//mouse用描画イベントオンオフスイッチ
this.mouseSwitch = {
  on: function(moveHandler, moveEndHandler) {
    $(self.canvas).on('mousemove', moveHandler).on('mouseup', moveEndHandler)
  },
  off: function() { $(self.canvas).off('mousemove').off('mouseup') },
}

// 手書き開始ハンドラーを戻す
// handlerSwitch: 描画イベントオンオフスイッチ
this.writeStart = function(handlerSwitch) {
  return function(e) {
  // 過処理防止措置
  // touchend後サーバーに手書きデータを送信するが、
  // 500ms以内の加筆は一旦キャンセルしてまとめて送る
  if (self.callTimer) window.clearTimeout(self.callTimer)
  var
    stroke = [], // タッチ開始から離すまでに書かれた曲線[{x,y,t:手書き開始からの経過時間},...]
    context = e.target.getContext('2d'),
    offsetX = $(e.target).offset().left,
    offsetY = $(e.target).offset().top,
    point = function(e) {
      return {
        x: e.pageX - offsetX,
        y: e.pageY - offsetY,
        t: (new Date).getTime() - self.startTime,
      }
    },
    // 手書き中ハンドラ(touchmove, mousemove)
    moveHandler = function(e) {
      var p = point(e)
      stroke.push(p)
      context.lineTo(p.x, p.y)
      context.stroke()
      e.preventDefault()
    },
    // 手書き終わりハンドラ(touchend, mouseup)
    moveEndHandler = function(e) {
      handlerSwitch.off()
      self.strokes.push(stroke)
      // 500ms後にサーバー送信
      self.callTimer = window.setTimeout(function() {
        callGoogleHandWrit(self.strokes, self.width, self.height, self.callback)
      }, 500)
      e.preventDefault()
    }
    handlerSwitch.on(moveHandler, moveEndHandler)
    0 == self.startTime && (self.startTime = (new Date).getTime())
    var p = point(e)
    stroke.push(p)
    context.lineWidth = 3
    context.lineCap = 'round'
    context.lineJoin = 'round'
    context.strokeStyle = self.strokeStyle
    context.beginPath()
    context.moveTo(p.x, p.y)
    context.lineTo(p.x, p.y+1)
    context.stroke()
    e.preventDefault()
  }
}

// 手書きデータのサーバー送信
// Googleのサービスを利用しているが、たまたまつかえているだけ
// いつAPIが変わるかもわからないし認証が必要になるかもしれないので利用注意
var callGoogleHandWrit = function(strokes, width, height, callback) {
  var ink = strokes.map(function(s) {
    var xs = [], ys = [], ts = []
    for (var j = 0; j < s.length; j++) {
      xs.push(s[j].x)
      ys.push(s[j].y)
      ts.push(s[j].t)
    }
    return [xs, ys, ts]
  }),
  api_param = {
    api_level: '537.36',
    app_version: 0.4,
    device: window.navigator.userAgent,
    input_type: 0,
    options: 'enable_pre_space',
    requests: [{
      writing_guide: {
        writing_area_width: width,
        writing_area_height: height
      },
      pre_context: '',
      max_completions: 0,
      max_num_results: 1,
      ink: ink
    }]
  }
  $.ajax({
    url: 'https://inputtools.google.com/request?itc=ja-t-i0-handwrit&app=chext',
    method: 'POST',
    contentType: 'application/json',
    data: JSON.stringify(api_param),
    dataType: 'json'
  }).done(callback)
}

// サーバー送信のコールバック
// 認識された文字をlabelに書き、このコンポーネントchangeイベントを発行する
this.callback = function(response) {
  if (response[0] && 'SUCCESS' == response[0] ) {
    var realize = response[1][0][1][0]
    self.value = realize
    self.oninput(self)
  } else {
    self.value = "error"
  }
  self.update()
}

// 手書きcanvasのクリア
this.clear = function(e) {
  var canvas = self.canvas
  canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)
  self.startTime = 0
  self.strokes = []
  self.value = ''
  self.oninput(self)
}

// DOM構築後の処理
this.on('mount', function() {
  // <canvas>はstyleでの幅指定ができないためJSで幅変更する
  var resize = function () {
      self.width=$(self.container).width()
      // リサイズするとcanvasの描画内容が消えるのでstateも合わせる
      self.strokes = []
      self.startTime = 0
      self.update()
    }
  window.setTimeout(resize, 0)
  var resizeTimer = null
  // Windowリサイズ時にもキャンバスの幅を変更する
  $(window).on("resize", function() {
    // 過処理防止措置
    if (resizeTimer) clearTimeout(resizeTimer)
    resizeTimer = setTimeout(function() {
      resize()
    }, 200)
  })
})
  </script>
</my-handwrite>