手書き文字を作れるJavascriptをつくってTensorFlowで予測させてみた(2)

この前、「手書き文字を作れるJavascriptをつくってTensorFlowで予測させてみた」という投稿でブラウザ上で手書きした文字画像を、MNISTで訓練したモデルで予測してみましたが、ものすごく精度が悪かったです。今回改めて、CNNを使ってやってみたらかなり精度が上がりました。何%か測ったりしてませんが、自分の手書きだと90%は超える感じでした。やっぱりCNNはすごいなーと思いました。でももしかしたら前回のものにミスがあり、CNNではなくても精度は本当はもっと高い可能性はあります。

もうちょっとやるとしたら、文字を画像の中心に適度な大きさで書く必要があり、例えば右上に小さく2と書いても認識されません。あとは、現在はMNISTに合わせて、手書き文字画像も背景黒、文字色白で作成するように固定していますが、これらの色を変えても認識するようにしたいです。今度やってみます。

Github

https://github.com/endoyuta/mnist_test

index.html

<html>
<head>
<title>MNIST TEST</title>
</head>
<body>
<h1>MNIST TEST</h1>
<canvas id="canvas1" width="400" height="400" style="border: 1px solid #999;"></canvas><br><br>
<input id="clear" type="button" value="Clear" onclick="canvasClear();">
<input id="submit" type="button" value="Submit" onclick="saveImg();"><br><br>
<img id="preview"><span id="answer"></span>
<script src="http://code.jquery.com/jquery-3.1.1.min.js"></script>
<script>
var url = 'http://127.0.0.1:8000/cgi-bin/mnist.py';
var lineWidth = 40;
var lineColor = '#ffffff';
var imgW = imgH = 28;

var canvas = document.getElementById('canvas1');
var ctx = canvas.getContext('2d');
var cleft = canvas.getBoundingClientRect().left;
var ctop = canvas.getBoundingClientRect().top;
var mouseX = mouseY = null;

canvasClear();
canvas.addEventListener('mousemove', mmove, false);
canvas.addEventListener('mousedown', mdown, false);
canvas.addEventListener('mouseup', mouseInit, false);
canvas.addEventListener('mouseout', mouseInit, false); 

function mmove(e){
    if (e.buttons == 1 || e.witch == 1) {
        draw(e.clientX - cleft, e.clientY - ctop);
    };
}

function mdown(e){
    draw(e.clientX - cleft, e.clientY - ctop);
}

function draw(x, y){
    ctx.beginPath();
    if(mouseX === null) ctx.moveTo(x, y);
    else ctx.moveTo(mouseX, mouseY);
    ctx.lineTo(x, y);
    ctx.lineCap = "round";
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = lineColor;
    ctx.stroke();
    mouseX = x;
    mouseY = y;
}

function mouseInit(){
    mouseX = mouseY = null;
}

function canvasClear(){
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    $('#preview').attr('src', '');
    $('#answer').empty();
}

function toImg(){
    var tmp = document.createElement('canvas');
    tmp.width = imgW;
    tmp.height = imgH;
    var tmpCtx = tmp.getContext('2d');
    tmpCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, imgW, imgH);
    var img = tmp.toDataURL('image/jpeg');
    $('#preview').attr('src', img)
    return img;
}

function saveImg(){
    var img = toImg();
    console.log(img);
    $.ajax({
        url: url,
        type: 'POST',
        data: {img: img},
        dataType: 'json',
        success: function(data){
            if(data.status){
                $('#answer').html('は、' + data.num + 'です');
            }else{
                $('#answer').html('は、分かりません');
            }
        },
    });
}
</script>
</body>
</html>

cgi-bin/mnist.py

#!/usr/bin/env python

import sys
import os
import cgi
import json
import cgitb
cgitb.enable()

from PIL import Image
import numpy as np
from io import BytesIO
from binascii import a2b_base64

import mytensor

print('Content-Type: text/json; charset=utf-8')
print()

if os.environ['REQUEST_METHOD'] == 'POST':
    data = cgi.FieldStorage()
    img_str = data.getvalue('img', None)
    if img_str:
        b64_str = img_str.split(',')[1]
        img = Image.open(BytesIO(a2b_base64(b64_str))).convert('L')
        img_arr = np.array(img).reshape(1, -1)
        img_arr = img_arr / 255
        result = mytensor.predict(img_arr)
        print(json.dumps({'status': True, 'num': result}))
        sys.exit()
print(json.dumps({'status': False, 'num': False}))

cgi-bin/mytensor.py

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

def _weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

def _bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

def _conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def interface():
    x = tf.placeholder(tf.float32, shape=[None, 784])
    y_ = tf.placeholder(tf.float32, shape=[None, 10])

    x_image = tf.reshape(x, [-1, 28, 28, 1])
    W_conv1 = _weight_variable([5, 5, 1, 32])
    b_conv1 = _bias_variable([32])
    h_conv1 = tf.nn.relu(_conv2d(x_image, W_conv1) + b_conv1)
    h_pool1 = tf.nn.max_pool(h_conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    W_conv2 = _weight_variable([5, 5, 32, 64])
    b_conv2 = _bias_variable([64])
    h_conv2 = tf.nn.relu(_conv2d(h_pool1, W_conv2) + b_conv2)
    h_pool2 = tf.nn.max_pool(h_conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    W_fc1 = _weight_variable([7 * 7 * 64, 1024])
    b_fc1 = _bias_variable([1024])
    h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
    h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

    keep_prob = tf.placeholder(tf.float32)
    h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    W_fc2 = _weight_variable([1024, 10])
    b_fc2 = _bias_variable([10])
    y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y_conv, y_))
    train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    saver = tf.train.Saver()

    class CNNModel():
        pass
    model = CNNModel()
    model.x = x
    model.y_ = y_
    model.keep_prob = keep_prob
    model.y_conv = y_conv
    model.train_step = train_step
    model.accuracy = accuracy
    model.saver = saver
    return model

def predict(img):
    ckpt = tf.train.get_checkpoint_state('./cgi-bin/ckpt')
    if not ckpt: return False
    m = interface()
    with tf.Session() as sess:
        m.saver.restore(sess, ckpt.model_checkpoint_path)
        result = sess.run(m.y_conv, feed_dict={m.x: img, m.keep_prob:1.0})
        return int(np.argmax(result))

def train():
    if tf.train.get_checkpoint_state('./ckpt'):
        print('train ok')
        return
    mnist = input_data.read_data_sets('./mnist', one_hot=True, dtype=tf.float32)
    m = interface()
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        for i in range(20000):
            batch = mnist.train.next_batch(100)
            m.train_step.run(feed_dict={m.x: batch[0], m.y_: batch[1], m.keep_prob: 0.5})
            if i % 100 == 0:
                train_accuracy = m.accuracy.eval(feed_dict={m.x:batch[0], m.y_: batch[1], m.keep_prob: 1.0})
                print("step %d, training accuracy %g"%(i, train_accuracy))
        m.saver.save(sess, './ckpt/model.ckpt')
    print('train ok')

if __name__ == '__main__':
    train()

手書き文字を作れるJavascriptをつくってTensorFlowで予測させてみた

MNISTでテストしているだけだと味気ないので、ブラウザで数字を手書きして、それを予測するようにしてみました。JavascriptのCanvasでお絵かきアプリみたいのを作って、そこに手書きで数字を書いてボタン押したら、28×28に縮小して、Base64の状態でサーバに送ります。サーバでPythonが、numpyの配列にして、TensorFlowに渡して推測しております。

多分やり方は大体あってるのではないかと思ってるのですが、いかんせん精度が超悪いです。そもそもまだTensorFlowがCNNになっていないのですが、それでもMNISTで学習・テストすると97%位にはなっているものです。背景黒・文字色白で0-255の明るさを28×28もつ配列にしているので、形式は合っているはずなのですが、やはり文字の太さとか大きさとかそういうのによって、全然違うということなのかなと思っております。ブラウザで割と適当に数字を書いてもいい感じの精度で答えを言ってくるのかなと期待していたので残念です。とりあえず、今度CNNで対応してみて、それでもダメだったら、Javascriptの画像自体で訓練をしていくようにしてみようかなと思ってます。(何か根本的な原因がありそうな気もしますが)

GitHub

一応GitHubに入れておきました。誰か精度よくしてくれたら嬉しいです。

追記(2017/01/25)
上記のリポジトリは、「手書き文字を作れるJavascriptをつくってTensorFlowで予測させてみた(2)」の内容に更新しました。

PythonでWEBサーバ起動して、必要なディレクトリ・ファイルを作成

$ mkdir mnist_test
$ cd mnist_test
$ python -m http.server --cgi
$ touch index.html
$ mkdir cgi-bin
$ touch cgi-bin/mnist.py
$ touch cgi-bin/mytensor.py
$ mkdir cgi-bin/mnist
$ mkdir cgi-bin/ckpt

各ファイルの中身を作成

index.html

<html>
<head>
<title>MNIST TEST</title>
</head>
<body>
<h1>MNIST TEST</h1>
<canvas id="canvas1" width="400" height="400" style="border: 1px solid #999;"></canvas><br><br>
<input id="clear" type="button" value="Clear" onclick="canvasClear();">
<input id="submit" type="button" value="Submit" onclick="saveImg();"><br><br>
<img id="preview"><span id="answer"></span>
<script src="http://code.jquery.com/jquery-3.1.1.min.js"></script>
<script>
var url = 'http://127.0.0.1:8000/cgi-bin/mnist.py';
var lineWidth = 50;
var lineColor = '#ffffff';
var imgW = imgH = 28;

var canvas = document.getElementById('canvas1');
var ctx = canvas.getContext('2d');
var cleft = canvas.getBoundingClientRect().left;
var ctop = canvas.getBoundingClientRect().top;
var mouseX = mouseY = null;

canvasClear();
canvas.addEventListener('mousemove', mmove, false);
canvas.addEventListener('mousedown', mdown, false);
canvas.addEventListener('mouseup', mouseInit, false);
canvas.addEventListener('mouseout', mouseInit, false); 

function mmove(e){
    if (e.buttons == 1 || e.witch == 1) {
        draw(e.clientX - cleft, e.clientY - ctop);
    };
}

function mdown(e){
    draw(e.clientX - cleft, e.clientY - ctop);
}

function draw(x, y){
    ctx.beginPath();
    if(mouseX === null) ctx.moveTo(x, y);
    else ctx.moveTo(mouseX, mouseY);
    ctx.lineTo(x, y);
    ctx.lineCap = "round";
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = lineColor;
    ctx.stroke();
    mouseX = x;
    mouseY = y;
}

function mouseInit(){
    mouseX = mouseY = null;
}

function canvasClear(){
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    $('#preview').attr('src', '');
    $('#answer').empty();
}

function toImg(){
    var tmp = document.createElement('canvas');
    tmp.width = imgW;
    tmp.height = imgH;
    var tmpCtx = tmp.getContext('2d');
    tmpCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, imgW, imgH);
    var img = tmp.toDataURL('image/jpeg');
    $('#preview').attr('src', img)
    return img;
}

function saveImg(){
    var img = toImg();
    console.log(img);
    $.ajax({
        url: url,
        type: 'POST',
        data: {img: img},
        dataType: 'json',
        success: function(data){
            if(data.status){
                $('#answer').html('は、' + data.num + 'です');
            }else{
                $('#answer').html('は、分かりません');
            }
        },
    });
}
</script>
</body>
</html>

cgi-bin/mnist.py

#!/usr/bin/env python

import sys
import os
import cgi
import json
import cgitb
cgitb.enable()

from PIL import Image
import numpy as np
from io import BytesIO
from binascii import a2b_base64

from mytensor import MyTensor

import logging

print('Content-Type: text/json; charset=utf-8')
print()

if os.environ['REQUEST_METHOD'] == 'POST':
    data = cgi.FieldStorage()
    img_str = data.getvalue('img', None)
    if img_str:
        b64_str = img_str.split(',')[1]
        img = Image.open(BytesIO(a2b_base64(b64_str))).convert('L')
        img_arr = np.array(img).reshape(1, -1)
        tf = MyTensor()
        result = tf.predict(img_arr)
        print(json.dumps({'status': True, 'num': result}))
        sys.exit()
print(json.dumps({'status': False, 'num': False}))

cgi-bin/mytensor.py

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
from PIL import Image

class MyTensor:
    H = 625
    BATCH_SIZE = 100
    DROP_OUT_RATE = 0.5

    def __init__(self):
        self.x = tf.placeholder(tf.float32, [None, 784])
        self.t = tf.placeholder(tf.float32, [None, 10])        
        self.w1 = tf.Variable(tf.random_normal([784, self.H], mean=0.0, stddev=0.05))
        self.b1 = tf.Variable(tf.zeros([self.H]))
        self.w2 = tf.Variable(tf.random_normal([self.H, self.H], mean=0.0, stddev=0.05))
        self.b2 = tf.Variable(tf.zeros([self.H]))
        self.w3 = tf.Variable(tf.random_normal([self.H, 10], mean=0.0, stddev=0.05))
        self.b3 = tf.Variable(tf.zeros([10]))

        self.a1 = tf.sigmoid(tf.matmul(self.x, self.w1) + self.b1)
        self.a2 = tf.sigmoid(tf.matmul(self.a1, self.w2) + self.b2)
        self.keep_prob = tf.placeholder(tf.float32)
        self.drop = tf.nn.dropout(self.a2, self.keep_prob)
        self.y = tf.nn.relu(tf.matmul(self.drop, self.w3) + self.b3)
        self.loss = tf.nn.l2_loss(self.y - self.t) / self.BATCH_SIZE

        self.train_step = tf.train.AdamOptimizer(1e-4).minimize(self.loss)
        self.correct = tf.equal(tf.argmax(self.y, 1), tf.argmax(self.t, 1))
        self.accuracy = tf.reduce_mean(tf.cast(self.correct, tf.float32))
        self.saver = tf.train.Saver()
        if not self.ckpt():
            self.train()

    def ckpt(self):
        return tf.train.get_checkpoint_state('.\cgi-bin\ckpt')        

    def predict(self, img):
        with tf.Session() as sess:
            self.saver.restore(sess, self.ckpt().model_checkpoint_path)
            result = sess.run(self.y, feed_dict={self.x: img, self.keep_prob:1.0})
            return int(np.argmax(result))

    def train(self):
        mnist = input_data.read_data_sets('.\cgi-bin\mnist', one_hot=True, dtype=tf.uint8)        
        with tf.Session() as sess:
            sess.run(tf.global_variables_initializer())
            for _ in range(20000):
                batch_x, batch_t = mnist.train.next_batch(100)
                sess.run(self.train_step, feed_dict={self.x: batch_x, self.t: batch_t, self.keep_prob:(1-self.DROP_OUT_RATE)})
            self.saver.save(sess, '.\cgi-bin\ckpt\model.ckpt')

ブラウザでアクセス

下記URLにアクセスすると、MNIST TESTというお絵かきページみたいのがでてきます。ちなみに、最初にsubmitボタン押すと、MNISTのダウンロードから学習までをするので、時間かかるし、結果も表示されません。2回目からはちゃんと結果が(一応)表示されます。

http://127.0.0.1:8000

Reactの開発環境

node.jsをインストールして、npmも最新版にする。
npmでプロジェクトつくって、下記をインストールする。

reactは、jdxを使うのが基本で、ES6の書き方でjdxを使って書いたものを、babelを使って、ES5のjsに変換する。webpackとbabelを連動させて、webpackで自動的に変換できるようにする。CSSはsassを使って書いて、これもwebpackを使って変換する。全てnpmでインストールできて、npmはpackage.jsonというファイルで、必要なパッケージとそれらのバージョンと依存関係を管理する。npmでインストールしたものは、自動的にpackage.jsonに反映される。必要に応じて手動でメンテナンスすることで、自動的な開発環境を構築する。あとはこれをgitで管理すればみんなで開発できる。大体こんな感じでやっていく。ReactはAngularと違って、オールインワンな感じのフレームワークではなく、基本viewに特化したフレームワーク?であり、大規模なシステムをつくる場合は、Reduxを使うといい。

あと、これらの開発環境セットアップをコマンド一発で完了できるcreate-react-appをfacebookが作ったらしい。

参考サイト:
10年のツケを支払ったフロント界隈におけるJavaScript開発環境(2016年4月現在)。
もうはじめよう、ES6~ECMAScript6の基本構文まとめ(JavaScript)~
npmとpackage.json使い方
webpackを使ってJSとCSSをコンパイルする(ES6 / Sass)
webpack で始めるイマドキのフロントエンド開発
babelとwebpackを使ってES6でreactを動かすまでのチュートリアル
Webpackでイチから作るReact.js開発環境
Reduxの実装とReactとの連携を超シンプルなサンプルを使って解説
コマンド一発でReactの開発環境を構築してくれるFacebook製ツール「create-react-app」

Node.js

Node.jsやってみる。

macは、El Capitan 10.11.5です。

ここでNode.jsをダウンロードしたり、ドキュメントを取得できたりする。
http://nodejs.jp/nodejs.org_ja/
Node.js v0.11.11 マニュアル & ドキュメンテーション

$ node -v
v0.11.11
$ npm -v
1.3.25

Node.jsは、サーバ側の言語。Nginxとかを使わないらしい。

//httpモジュールをインポート
var http = require('http');
 
//Webサーバーの設定
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(3000);
 
console.log('Server running at http://localhost:3000/');

(引用:いまさら聞けないNode.jsの基礎知識とnpm、Gulpのインストール (1/2)

これでWEBサーバ作ったことになる。Node.jsはWEBサーバで、nginx.confみたいなのが上記なんでしょうか??とりあえずやってみる。

~/nodejs/app.jsを作って、上記を書いた。その後、下記を実行。

$ node app.js
Server running at http://localhost:3000/

おー表示された。

nodejs

モジュール管理は、npmでやる。
ビルドツールは、GruntとかGulpとかでやるんだけど、最近はちょっと嫌われているようで、使わないでやるケースも多いらしい。Gulpのがまだ人気あるらしい。

npmでgulpをグローバルインストールする

$ npm install -g gulp

gulp用ディレクトリを作って、そこにgulpをインストールする

$ npm install gulp
$ gulp -v
[03:12:17] CLI version 3.9.1
[03:12:17] Local version 3.9.1

gulpfile.jsを作成してみる。

'use strict'
var gulp = require('gulp');
 
//watchタスク定義
gulp.task('watch', function () {
    //ファイルが変更されたらメッセージ表示
    gulp.watch('*', function (event) {
        console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
    });
});
 
//デフォルトタスクとしてwatch実行
gulp.task('default', function () {
    gulp.run('watch');
});

gulp実行

$ gulp

ファイルを追加したり、変更したり、削除したりすると、eventを検知して、event.path、event.typeとかが取得できる。これをもとにテストしたり、コンパイルしたりするんだろう。

ただ、上記コードだと、結構な頻度でエラーが出る。try-catchとかしっかりしたらOKになるのだろうか。

events.js:82
throw er; // Unhandled 'error' event

ここにあるコンテンツとドットインストールみたらいいかも。
「Node.jsでサーバサイドJavaScript開発入門」

Backbone.js(Marionette) – CollectionViewとCompositeView

ItemViewはシンプルだけど、collectionViewとcompositeViewというのがあるので、使い分けを確認する。

参考1:Marionette.jsまとめ その3 CompositeView, Layout, Region

CompositeViewは、テンプレートを設定することができるらしい。CollectionViewは、テンプレートを設定できない。では、CollectionViewの一般的使い方を確認します。

CollectionViewの使い方

参考2:Marionette.jsまとめ その2 View, ItemView, CollectionView

下記は、上記参考2の引用です。

var FooItemView = Marionette.ItemView.extend({
    template: '#itemTemplate'
});
var FooCollectionView = Marionette.CollectionView.extend({
    el: '#fooList',
    itemView: FooItemView
});

ItemViewをセットして、CollectionViewが紐づくエレメントを指定する感じです。#fooList内に、#itemTemplateがcollectionが保持するModel分出力される感じになります。単純ではあります。

テンプレートをもうひとはさみしたい的な感じに、CompositeViewを使います。

CompositeViewの使い方

下記は、上記参考1の引用です。

var FooItemView = Marionette.ItemView.extend({
    template: '#itemTemplate'
});
var FooCompositeView = Marionette.CompositeView.extend({
    el: '#main',
    itemView: FooItemView,
    itemViewContainer: '#fooList',
    template: '#listTemplate'
});

多分、#listTemplateの中にある、#fooListの中に、collectionが保持するModel数分、FooItemViewを出力されます。そして、#listTempalteが、#mainの中に出力されます。って感じだと思う。

ちなみに、#foolistが、<ul>の場合、ItemViewのtagNameは、”li”にする必要があります。tagNameを設定しないとデフォルトでは、<div>で囲われますので、下記みたいな感じになってしまいます。

<ul><div>hoge</div>....</ul>

画面の高さに画像サイズを合わせる

トップページのでっかい画像を画面の高さに合わせることで、スマホでもPCでも最初の表示は画面全体に画像が表示されるようにしたい。

画面の高さは、$(window).height()なんじゃないかと思っている。だから最初に、$(‘#top_img’).height($(window).height())みたいにすればいいだけなのではないかと思っております。しかし、画面リサイズ時の画像サイズ調整も考えた方がいいかもしれない。画像を入れるdivと、画像自体のサイズを変更しないといけないかもしれない。

HTML

<div class="top_box_before">
	<div class="top_box_content"></div>
</div>
<div class="top_box"></div>

CSS

.top_box, .top_box_before{
    height:600px;
}
.top_box{
    position: relative;
    width:100%;
    background-color: #fff ;
    background:no-repeat center center;
    background-size:cover;
    z-index: 0;
}
.top_box_before{
    position: absolute;
    background: rgba(33, 33, 33, 0.3);
    z-index: 1;
    width:100%;
    top:40px;
    display:table;
}

.top_boxの背景画像は、別のところで設定している。

Javascript

var h = $(window).height();
$('.top_box').height( h + 'px');
$('.top_box_before').height( h + 'px');

これだけ。画像リサイズは考慮しなくていいと思った。

スマホでfixedをサポートするのはめんどくさい

参考:
iPhone、Android position:fixed 対応状況と対応方法
Fixed固定ナビゲーションを設置するときに気をつけたい4つのこと
user-scalable=noを使う理由と弊害(スマホのviewportを見直す)

スマホではfixedがサポートされていないのがある。ios5から対応していて、Android 4.1は対応しているらしい。Android 2.3は、viewport で、content=”user-scalable=no” にすると対応されるらしい。あら、自分のAndroidは、4.1.2となってるが、fixedには対応されていないな。user-scalable=noを設定してもうまくうごかない。

今時点では確かにfixed使うと問題が多いなあ。解決は、jQuery Mobileとか、iScrollとかいうのを使う必要があるらしい。Bootstrapは大丈夫だった気がするが、あれは上記のようなツールと同じように、javascript含めて色々な調整がされているのかもしれない。

とりあえず、スマホでfixed使うのやめることにしよう。画面狭くなるしな。

ページ内ヌルヌル

ページ内 ヌルヌルでgoogle検索するといっぱいでてきます。ヌルヌルって言い出したのはだれなんでしょうか。

http://webnonotes.com/javascript-2/pagescroll/これが一番上に出てきます。

this.hashこれで、ハッシュの要素を取得できるようです。
$(hash).offset().topこれで位置を取得できるようです。
$(‘html,body’).animate({scrollTop: offset}, 800);これでヌルヌル移動できるようです。800がスピードでしょう。

これだと、URLは変わらないのか。backborn使ったときみたいなURLも変えつつ、移動したいな。トップに戻るとかならこれで全然いいんだな。とりあえず、ついにページトップに戻るボタンをつくりたいと思います。

トップにヌルヌル戻る

HTMLのどこかに、このようなものを作ります。

<div id="back_top" onclick="back_top();"></div>

そして、このようなjavascriptを作ります。

function back_top(){
    $("html,body").animate({scrollTop: 0}, 800);
}

これだけです。画面のどこにいるかを確認して、上から500pxくらいになったらTOPに戻るボタンを表示するようにしたい。

上から500pxになったらTOPに戻るボタンを表示する

$(window).scroll(function () {
    if($(this).scrollTop() > 500){
        $('#back_top').fadeIn();
    }else{
        $('#back_top').fadeOut();
    }
});

これだけです。

ページ内ヌルヌル移動

クリックしたらuRL変わるようにしたいし、URL変えつつヌルヌルするのめんどくさいからやめよう。

Backbone.jsとMarionette

http://backbonejs.org/ここのdevelopバージョンを使ってみます。Underscore.jsに依存します。http://underscorejs.org/
jQueryも使います。

モデル

var Hoge = Backbone.Model.extend({
    defaults:{
        'name': 'taro',
        'age': 20,
        'updateTime': new Date()
    },
    initialize: function(){
        console.log('create hoge : ' + this.cid + ' : ' + JSON.stringify(this));
    }
});

var hoge = new Hoge();
hoge.set({
    name: 'jiro',
    age: 30
});

console.log('hoge : ' + hoge.cid + ' : ' + JSON.stringify(hoge));

こんな感じで、モデルを作成できる。defaultsで初期設定、initializeでコンストラクタの設定ができる。newで、モデルのインスタンスを作成し、setで、各値をインスタンスにセットできる。getで取得できる。hoge.get(‘name’);

var hoge = new Hoge({
    'name': 'saburo',
    'age': 30
});

こんな風に、newのときに、引数に値を渡すことでセットすることもできる。newでインスタンスを作成すると、cidというものが自動的に設定される。これはインスタンスを一意に識別するためのもの。

hoge2 = hoge.clone();

clone()で、インスタンスを複製できる。cidだけは、ユニークとなる。

その他、モデルのメソッド・プロパティは、attributes、clear、has、unsetがある。

モデルに関数を追加するのはこんな感じでできる。

var Hoge = Backbone.Model.extend({
    defaults:{
        'name': 'taro',
        'age': 10,
        'updateTime': new Date()
    },

    initialize: function(){
        console.log('create hoge : ' + this.cid + ' : ' + JSON.stringify(this));
    },

    add_age: function(){
        this.set({age: this.get('age') + 1});
    }
});

var hoge = new Hoge();
hoge.set({
    name: 'jiro',
    age: 20
});
console.log(hoge.get('name') + ' : ' + hoge.get('age'));

hoge.add_age();
console.log(hoge.get('name') + ' : ' + hoge.get('age'));

属性の値を1追加するのに、こんなめんどうな書き方しかできないとは思えないんだけど、this.age ++;とかではエラーになった。
その他、モデルの変化に応じてイベントを発動したり、どこが変化したのかチェックしたり、モデルの変化が妥当な変化かをチェックするバリデーションルールの設定などができる。

モデルのサーバとのやりとり

はて、Ajaxでサーバとやりとりするのも、Bacbone.jsは便利になっているもよう。サーバ側をcakePHPでつくりながら試してみる。

モデルで、サーバとやりとりする際は、APIのURLを設定する。

var Dev = Backbone.Model.extend({
    urlRoot: 'http://local.com/devs/hoge/'
});

あとは、saveメソッドを呼出すだけで、新規作成か更新処理をしてくれるということで、作成処理はPOSTアクセス、更新処理は、PUTアクセスする。saveメソッド呼び出し時に、id属性の指定があれば、更新処理とし、なければ新規作成とする。らしい。

便利そうでありつつ、色々注意が必要そうですが、一旦データ取得から確認してみたい。データ取得は、fetchメソッドらしい。

参考:試して学ぶ Backbone.js入門2この記事は分かり易そう。

cakePHPのcontrollerで下記のようなものをつくりました。Devモデルというのを既に作成済みです。

public function hoge($id = null){
	if(!$this->request->is('ajax')) throw new BadRequestException();
	$this->autoRender = false;

	//取得(fetch)
	if($this->request->is('get')){
		if($id){
			$devs = $this->Dev->findById($id);
		}else{
			$devs = $this->Dev->find('all');
		}
		return json_encode(compact('devs'));
	}

	return null;
}

これにfetchアクセスする、backboneのコードをつくってみます。

var Dev = Backbone.Model.extend({
    urlRoot: 'http://local.com/backborn/devs/hoge/'
});

var dev = new Dev();
dev.fetch({
    success: function(model, response, options){
        console.log(response.devs[0]['Dev']['title']);
    }
});

これでできました。idを渡してみます。

var Dev = Backbone.Model.extend({
    urlRoot: 'http://local.com/backborn/devs/hoge/'
});

var dev = new Dev();
dev.set({id: 3});
dev.fetch({
    success: function(model, response, options){
        console.log(response.devs['Dev']['title']);
    }
});

できました。分かりました。取得後に、parseというのを使って、レスポンスに対して色々やったりするらしい。

コレクション

コレクションは、モデルのリストらしい。

View

viewは、モデルの変更に目を光らせて、レンダリングするやつらしい。モデルが変更されたらBackbone.Eventsによって、Viewに通知されて、その通知内容に基づいてレンダリングする内容をつくるらしい。そして、テンプレートに渡すという流れのようでありんす。つまり、cakePHPのモデルとbackboneのモデルは同じだけど、cakePHPのcontrollerとbackboneのviewが同じで、cakePHPのviewとbackboneのテンプレートが同じみたいな感じかなと思った。

Viewは、モデルやコレクションの変更に目を光らせるだけではなく、DOMの変更にも目を光らせる。そして、一つのViewは、一つのDOMに紐づける必要がある。DOMとの紐づけは、elプロパティが使える。紐づくDOMの条件をelの値として設定することができる。その他、tagName、className、id、attributesなどを設定することで、動的にel属性を作成することができる。elとtagNameとかが同時に設定されている場合、elの設定が優先されるし、el設定が存在しないdomを表している場合は、tagName等が設定されていても、elはundifinedになる。elを設定されている場合は、必ず存在するDOM条件を設定する必要があるようだ。elもtagName等も設定されていない場合は、空のdivがel属性の値になる。新たに作成されたel属性の場合は、el属性が作成された段階ではDOMに追加されないらしい。

HTML

<div id="hoge"></div>

Js

var DevView = Backbone.View.extend({
    el: '#hoge'
});

var dev_view = new DevView();
console.log(dev_view.el);

これのログへの出力結果は、<div id=”hoge”></div>となる。

viewのコンテンツを表示するには、renderメソッドを使う。

var DevView = Backbone.View.extend({
    el: '.hoge',
    render: function(){
        this.$el.append('hogehoge');
        return this;
    }
});

var dev_view = new DevView();
dev_view.render();

テンプレートは、Backbone.js自体に標準搭載されているわけではなく、一番シンプルなのは、Underscore.jsのテンプレート機能を利用することらしい。簡単そうなので、後で調べる。次にルーター機能について確認したいと思います。

ルーター

var MyRouter = Backbone.Router.extend({
    routes: {
        'hoge': 'hoge',
        'aiu': 'aiueo'
    },

    hoge: function(){
        console.log('hogehogehoge');
    },

    aiueo: function(){
        console.log('aiueoaiueo');
    }
});

var router = new MyRouter();
Backbone.history.start();

こんな感じでつくる。ということは、まずルーターのroutesがURLを検証して、何かに合致したら該当する関数を呼出すところからアプリケーションが始まるわけであります。そこから、viewを作って、そこで色々モデルを使いながら表示する内容をつくって、テンプレートに渡して表示するわけであります。実際的にはViewは、cakePHPのレイアウトとか、view、エレメントとかに分けて作成してそれを組み合わせていくわけであります。

Marionette

http://marionettejs.com/ Backbone.jsのコードをよりシンプルに、管理し易くしてくれるやつらしい。Backbone.js使うなら使った方がいいらしい。

参考:
Marionette.jsまとめ その1 Application, Controller, AppRouter
実践Backbone.Marionette 現場の悩みと解決まで
“Backbone.Marionette.js: A Gentle Introduction” を今更ながら勉強してみた

上記3つを見ると色々分かりそうだなーと思いました。ありたがいです。

jquery – テーブルに検索、ソート、ページネーション機能をもたせるDataTablesプラグイン

DataTables

fuelphpの検索結果をテーブルにして、ソートとページネーションをAjaxな感じでやりたいので、これを使ってみる。下記サイトをみて知った。

DataTables(日本語で紹介してるサイト)

ダウンロードはここでできた。

おお、お手軽だ。さすがプラグイン。
プラグインを読み込んで、tableにidを設定して、下記のようにすればできた。tableのidが#resultの場合の例。

$(document).ready(function() {
    $('#result').DataTable({
        searching: false
    });
} );

optionについては、http://www.datatables.net/reference/option/に色々書いてある。searching:falseは、検索ボックスを非表示にしている。

HTML5 Canvas のIE8対応

最初ExplorerCanvasを使ってしまって、全然動かなかった。

参考:Internet Explorer 8でCanvasを動かす時のメモ

VMLCanvasを使うとよかったです。
https://code.google.com/p/mofmof-js/wiki/VMLCanvas

VMLCanvas.js は uuCanvas.js から Silverlight と Flash バックエンドを省略し、 コンパクトにパッケージしなおした JavaScript ライブラリです。mofmof.js に依存せず単体でも動作します。 ExplorerCanvas に比べ、より多くの機能をサポートしています。

canvasでグラフを表示してマウスオーバーでポップアップするようなものでしたが、結果的には全てIE10や、chromeなどと同じように表示することができました。

IEだけVML Canvasを読み込みます。

<!--[if IE]>
<?php echo $this->Html->script('VMLCanvas-1.1.1.min')?>
<![endif]-->

ie8かチェックする関数をつくります。

function check_ie8(){
	var userAgent = window.navigator.userAgent.toLowerCase();

	if (userAgent.indexOf('msie') != -1) {
		var appVersion=window.navigator.appVersion.toLowerCase();

		if (appVersion.indexOf("msie 6.") != -1) {
			return true;
		} else if (appVersion.indexOf("msie 7.") != -1) {
			return true;
		} else if (appVersion.indexOf("msie 8.") != -1) {
			return true;
		}else{
			false;
		}
	}else{
		return false;
	}
}

ie8以外の場合は、$(window).loadを使い、ie8の場合は、window.oncanvasreadyを使います。

$(window).load(function () {
	if(check_ie8()) return;

	canvas = document.getElementById("hoge_canvas");
	C = canvas.getContext("2d");
	init();
	images[num].onload = function(){
		draw();
		timerID = setInterval ('check_on_mouse()', 33);
	}
});

window.oncanvasready = function(canvasNodeList) {
	if(!check_ie8()) return;
	ie8 = true;

	canvas = document.getElementById("hoge_canvas");
	C = canvasNodeList[0].getContext("2d");

	init();
	draw();
	timerID = setInterval ('check_on_mouse()', 33);
};

これで全部表示されました。スクリプト分けるとメンテが大変なので一つにしようと思って上記のようにしました。images.onloadを使うとie8で動かなかったのでとりあえず外しました。

マウスオーバーチェックは、下記のようになりました。

function check_on_mouse(){
	canvas.onmousemove = getMousePoint;

	function getMousePoint (e) {
		if(ie8){
			e = event;
			mouse_x = e.x;
			mouse_y = e.y;
		}else{
			var rect = e.target.getBoundingClientRect();
			mouse_x = e.clientX - rect.left;
			mouse_y = e.clientY - rect.top;
		}

		//mouse_xとmouse_yがプロットされてるアイテムに重なってればポップアップする処理など
	}
}

ie8の場合、e.targetというのが使えないので、その変わりにsrcElementというのを使うのですが、それでやってもrectの値がおかしくて使えませんでした。e.xがmouse_xとほぼ同じになるので、上記のようにしました。

画像をクライアント側で縮小する

スマホからアップロードしようとするとスマホで撮影した画像ファイルのサイズが大きいので大体エラーになります。クライアント側で縮小しつつ、向きも合わせるとう処理が必須であります。

画像アップロード前にクライアント側で縮小してプレビューし、アップロード

ここにそのまんまのことが書いてあります。ありがたい。これをやってみたいと思います。幅・高さの最大値を決めてそれより大きい場合縮小するということをしてます。iPhoneの対策なんかもしております。リサイズしたものをFormdataにいれてAjaxで登録しております。

FormDataからFormに書き込むことはできないらしい。ということはAjax不要のFormでも、クライアント側リサイズする場合、全部Ajaxに変えないといけないっぽい。めんどい。

今Ajaxでやってるんだけど、cakephpの場合、fileだけ$this->request->params[‘form’]に入ってる。

やっとできた。

chrome window.printが効かない

謎の現象がおきています。localのchromeであれば問題なくうごきますが、さくらサーバでやると動きません。といっても他のページだと問題なく動くのですが特定ページのみ、さくらサーバだとwindow.print()が効きません。厳密に言うと、window.printを実行してもうんともすんともいわないものの、その後、そのページから離れる直前に、printプレビュー画面が表示されます。。。なぜ??

safariでもfirefoxでも大丈夫なのにchromeだけ変だ。window.print実行後に別のページ行くか、リロードするかしようとすると表示される。なんなんだこれは。

<a href="javascript:hoge();">print</a>
<script>
function hoge(){alert(123);window.print();alert(324)}
</script>

とかってやると、alert()は表示される。どちらも。でもpreviewだけリロードとかしないと表示されない。あらローカルだとpreviewは表示されるが、逆にalertが表示されない。なんだこりゃ。

なんとchromeを終了して再度開きましたら、なおりました。。

いい感じにくっつけたフッターとヘッダーの間にあるコンテンツの背景を白く塗りつぶしたい

ヘッダーとフッターの間にあるコンテンツ空間はコンテンツ量によって高さが変動しますので、普通にbackgroundの背景色を設定しても、コンテンツ量が少ない場合、余白が出来てしまいます。この余白をださずに背景色で埋めたいです。

display: flexってやつが使えないだろうかと思いましたが、flexでコンテンツかこっちゃうというのは今更現実的でないので、結局javascript使っちゃうことにしました。

$(document).ready(function(){
    var f = 94;
    var height = $('#wrap').height();
    var h = $('#header').height();
    $('#contents').height(height - h - f);
});

f = 94というのは、コンテンツのfooterと重ならないようにするためのpadding-bottomの値になります。jqueryのheight関数は、paddingを除いた高さのことみたいです。

他のところをクリックしたら閉じる javascript

下のスクリプトでやったらパソコンとAndroidはうまく閉じるけど、iPhoneで試したら閉じない。

function toggle_sub_menu(){
	$('#sub_menu').slideToggle(300);
	event.stopPropagation();
}

$(document).on('click', 'body', function(e){
	if(!$(e.target).is('.el-icon-lines')){
		if(!$(e.target).is('#sub_menu') && !$(e.target).closest('#sub_menu').size()){
			if($('#sub_menu').is(':visible')){
				$('#sub_menu').slideUp(200);
			}
		}
	}
});

safariも大丈夫だけどiPhoneだけだめで、bodyのクリックイベントを受けてないようだ。
iPhoneのclickイベントの挙動

ここに色々書いてくれてるのでこれみたら解決しそう。
sub_menu見えてるかチェック先にやった方がいいか。