todays!の仕様解説

2015年3月21日 by hinata

・なぜ午前5時でログがリセットされるの?
 
 投稿された内容がいつでもどこまででも遡れるのは確かに便利ですが、全部見れてしまうのは不都合が生じることもあります。
 個人情報に関わる投稿、性癖にまつわるやや他人には理解されにくい発言、他人の悪口等、こういったものは自分で一つ一つチェックして
 削除していかないと消えないのは逆に不便なことも多いです。
 なので一定期間以上は保存されない、という仕組みがあったらいいんじゃないか、という発想で作りました。

・なぜフォローが常に承認制で相互フォローのみなの?

 ツイッターなどでは相手が自分の知らないうちに自分をフォローすることができます。
 ですが、鍵を自分のアカウントにかけていない限り、相手が知らないうちに自分をフォローしていて自分の投稿を
 いつでも見れる状態になっている、というのは人間関係という観点でいびつですし、
 全ての投稿を責任を持って世界中の誰でもいつでも閲覧できるということを常に念頭において発言していれば
 問題は生じないでしょうが、ほとんどの人はそうではありません。
 であるならば、投稿を見れるのは「自分が承認したユーザのみ」であるべきです。
 こういった考えに基づいて、フォローを承認制のみとしています。
 相互フォローのみなのは、 「相手がフォローリクエストして、自分が承認したなら、相手が自分の投稿を見れるだけでなく自分も相手の投稿を見れなければフェアではない」
 という考えに基づいています。

・なぜフォローリクエストを「してきたユーザがリクエストした先のユーザのタイムラインを見れる」のではなく逆にしているの?

 これは人間関係という観点からこういう仕様にしています。
 フォローリクエストを行った人間が承認を得る前にリクエスト先の投稿を見れてしまうのは仕様的に整合性がとれていないのも理由です。
 相手をフォローしたいと思うなら、「人に名を聞くなら自分がまず名乗れ」という言葉があるように、
 自分の投稿をまず相手に公開するべきだという考えです。
 但し、リクエストが承認されるまではフォローされている状態ではないですから、
 「リクエストが承認されないまま24時間が経過したら見れない状態に自動的に戻る」仕様にしています。

・なぜRTの機能がないの?
 
 RTの機能を搭載する場合、フォロー外ユーザからのRTの閲覧権限の扱いが問題となります。
 フォロー承認制の仕様に従うなら「RTもフォローしているか自分がフォローリクエストを送っているユーザからしか見えてはいけない」
 べきですが、これだと「自分をフォローしているが自分がフォローしているユーザをフォローしていない」
 ケースで、後者のユーザのRTを自分をフォローしているが後者のユーザはフォローしていないユーザは見れないことになります。
 そして、後者のユーザも自分もフォローしているユーザにはRTも見れます。
 結果何が起きるか?「自分をフォローしているユーザ同士でRTが見れるユーザと見れないユーザ」が出てきます。
 こうなると、見れないRTに対してタイムライン中で言及したりしだして、会話の流れがわかりにくくなります。
 リプライであれば(いわゆるエアリプは別ですが)@にIDが続いていることで他人に対して何かを言っているということが
 リプライ先の投稿が自分のタイムライン上で見えなくても察することができますが、
 RTに対するリプライでない言及は意味不明です。
 こういった理由から、RTの機能をあえてつけないことにしました。

・なぜフォロー外ユーザへのリプライが見えるの(リプライ先は見えません)?

 RTの機能を外した結果、フォロー外ユーザとの繋がりができにくい問題が生じます。
 そこで、あえてフォロー外ユーザに対するリプライの投稿をタイムライン上に出すことにより、
 「何か知らない人に話しかけているな?」ということがわかるようにしました。
 こうすることで、その話しかけている相手に対して興味を持ってもらい、ユーザのプロフィールを表示してフォローする、という流れが生じる可能性が生まれます。

・なぜ投稿をお気に入りに入れる機能がないの?

 ログが一日しか保存されない仕様で、お気に入り機能があってもあまり意味がないので、こういう仕様にしました。

https://todays-log.com

todays!とは?

2015年3月21日 by hinata

todays!は、毎日日本時間午前5時で前日の投稿全てがリセットされるミニブログです。 投稿はあなたがフォローしている相手と あなたがフォローリクエストを送っている相手にしか見えません。 フォローは承認制で、フォローリクエストを投げられたユーザが承認して初めて成立します。 ※この時点で自動的に相互フォローとなります。
https://todays-log.com

スマートフォンを意識したUI設計において意識すべきこと

2015年3月18日 by hinata

– 小さいスペースにあまり色々詰め込まない
– 文字は楽に読めるサイズに。小さくても14pxまで。
– ブラウザのデフォルトの縦スクロール機能を活用する。
– UIの使い方は極力、見た目で直感的にわかるように。
– 見た目だけでわからない操作はヘルプに書かず、UI自体に説明を組み込む。
– 一画面内にUIが入りきらない時はフリックスクロールかスタック式のウィンドウに分割する。

フォローは常に相互フォローの承認制、毎日午前5時でログがリセットされるツイッター風SNS、todays!をリリースしました。

2015年3月18日 by hinata

「ネットで過去の情報がどこまでも遡り放題、検索し放題なのは確かに便利だけど、なんでもかんでも”見えてしまう”のは逆に不便なんじゃない?」
という思いから、「毎日午前5時で前日までの書き込みがリセットされる」という発想のSNSを作りました。
フォローは常に相互フォロー、フォローリクエストを承認した時点で自動的に相互フォローが成立。
フォローリクエストを送るとリクエストを「送られた側が送った側のタイムラインを24時間だけ」リクエストを承認するまでの間見ることができるようになります。

URLは以下。
https://todays-log.com

タッチイベントでもマウスイベントでも操作できるUIを作る実装方法

2015年3月18日 by hinata
  • 序文
  • スマートフォンの普及によってタッチイベントをJavascriptから扱うようになって久しいですが、
    世の中には「タッチイベントもマウスイベントもサポートしている端末」というものがあります。
    そして、こういう端末はマウスでクリックすると「touchstartに続けてmousedownイベントが自動で発火する」
    という困った特徴があったりします。
    windows8もその一つです。
    マウスでもタッチでも操作できるのがこういう端末のUIとしては理想ですが、実装が難しく、
    タッチのみに対応させるのが関の山でした(タッチをサポートしてる端末を判定してタッチで処理させる実装)
    マウスのみサポートだと、スマートフォンで動きませんからね。。。

    しかし、ようやく、この問題をどうにかクリアできる実装を考え出せたっぽいので、ここに公開します。

  • イベントはtouchstart⇒mousedownの順で発生するっぽい
  • なので、まずイベントハンドラの一つ外側のスコープに即時関数を定義して、
    そこにイベントタイプ記録用の変数を用意してnullで初期化します。

  • イベントタイプを判定する
  • イベントハンドラ先頭で保存したイベントタイプ文字列がtouchstartで、かつ現在のイベントタイプがmousedownか判定する。
    判定処理を先に書いて判定結果が真なら何もせずにreturnする(必要に応じてpreventDefaultも実行)

  • 現在のイベントタイプ文字列を保存する
  • イベントハンドラの引数にeを書いてe.typeで取れます。

  • 保存したイベントタイプをnullクリア
  • ここが重要です。
    クリアしないと次回以降正常動作してくれませんが、クリアをすぐに行ってはいけません。
    クリア後にmousedownが発生してしまいます。

  • setTimeout!
  • はい。「300ミリ秒後」かそれ以上後の時間を設定してsetTimeout内でクリアします。
    これで最初のイベントだけを処理対象にできます。

  • 応用
  • フリックスクロールをタッチ、マウス両方で行う方法です。

    イベントタイプの保存は同じですが、
    touchstart、mousedown時に保存したイベントタイプを
    「touchendかmouseup」でnullクリアします。
    ※タッチ、マウスの座標の取り方の違いを吸収する実装も忘れずに!
    さらに、touchmoveでは保存したイベントタイプがtouchstartである場合だけ、
    mousemoveでは保存したイベントタイプがmousedownである場合だけ処理を行います。
    touchend、mouseupも同様に保存したイベントタイプを見て対応するデバイスのイベントだった場合のみ処理を行います。

  • 実際の利用例
  • 僕の公開しているSNSのタイムラインクリック時にメニューを開く実装で利用しています。
    (ブラウザのウィンドウ内の高さが狭くなって「返信」ボタンが消えてる状態で機能します)

    SNSは以下。

    todays!

FuelPHPテスト実行時、\Request::$mainが初回のforge時の状態から更新されない

2015年1月20日 by hinata

コアクラスを拡張し以下のメソッドを定義、コントローラのテスト実行前に呼び出し

public static function reset()
{
    static::$main = false;
    static::$active = false;
}

FuelPHPでテスト時にassetを有効にする

2015年1月20日 by hinata

以下のコードをテスト実行前に実行(setUpなどで実行してしまうとよい。)

foreach(["js", "css", "img", "fonts"] as $type)
{
    \Asset::instance()->add_type($type, realpath(APPPATH."../../public/assets/{$type}"));
}

php5.4で即時関数

2013年7月28日 by hinata

javascriptでよく使われる即時関数をPHPでやる方法。

<?php
    call_user_func(function ($v) {
        echo $v;
    }, "aaaa");
?>

call_user_funcを使います。簡単ですね。

XMLHTTPRequestとiframeとpostMessageでクロスドメイン通信

2012年11月23日 by admin

http://vps.will-co21.net/test/html5proxy/postMessageCrossDomain.html?/formdatapost/tmp/hoge.png
html5のpostMessageとXMLHTTPRequestとiframeを組み合わせて画像のバイナリをXMLHTTPRequestで取得して
base64エンコードしたものをiframeからpostMessageで別ドメインの親ウィンドウに送って
dataURLにして画像として表示。
親ウィンドウ
window.onload = function () {
    require(["base64"], function (lib ) {
        var base64 = lib.base64;
       
        var iframe = document.createElement("iframe");
        iframe.src = "http://habrashi.s351.xrea.com/htmlproxy/proxy.html?" + encodeURIComponent(window.location.search.substr(1));
        iframe.setAttribute("id", "xhrframe");
        document.body.appendChild(iframe);
       
        window.addEventListener("message", function (e) {
            var data = e.data;
            img = document.createElement("img");
            img.src = "data:image/png;base64," + data;
            document.getElementById("content").appendChild(img);
        });
    });
}

window.onload = function () {
    var target = (parent && parent.postMessage ? parent : (parent && parent.document.postMessage ? parent.document : undefined));
    require(["base64"], function (lib ) {
        var xhr = new XMLHttpRequest();
        var base64 = lib.base64;

        xhr.onreadystatechange = function () {
            if(xhr.readyState == 4){
                var res = xhr.responseText;
                var bytes = [];
               
                for(var i=0, len = res.length; i < len; i++) bytes.push(String.fromCharCode(res.charCodeAt(i) & 0xff));
                var data = bytes.join("");
               
                base64String = base64.encode(data);
               
                if (typeof target != "undefined") {
                    target.postMessage(base64String, '*');
                }
            }
        }
        var url = decodeURIComponent(window.location.search.substr(1));
        xhr.open("GET", url, true);
        xhr.overrideMimeType("text/plain; charset=x-user-defined");
        xhr.send(null);
    });
}

子ウィンドウ
※require.jsとsmokescreenで使われてるbase64ライブラリを使用。
子ウィンドウをPHPにしてiframeをtargetとしてフォームデータをPOSTして、
POSTされたデータをJSONエンコードしてJSに渡せば、
POSTデータの中継なんかもできそう。

globalCompositeOperation=”darker”が使えないブラウザ用の自前JS実装改良版

2012年11月18日 by admin

function darker(src, dst, globalAlpha)
{  
    var w,h;
   
    if(src.width > dst.width)
    {
        w = src.width;
    }
    else
    {
        w = dst.width;
    }
   
    if(src.height > dst.height)
    {
        h = src.height;
    }
    else
    {
        h = dst.height;
    }

    var ctx1 = src.getContext("2d");
    var canvas = document.createElement("canvas");
    canvas.setAttribute("width", w);
    canvas.setAttribute("height", h);
    ctx2 = canvas.getContext("2d");
    ctx2.drawImage(src, 0, 0);

    var canvas2 = document.createElement("canvas");
    canvas2.setAttribute("width", w);
    canvas2.setAttribute("height", h);
    ctx3 = canvas2.getContext("2d");
    ctx3.drawImage(dst, 0, 0);

    var result = ctx2.getImageData(0, 0, w, h);
    var blend  = ctx3.getImageData(0, 0, w, h);
    var alpha;

    for(var data = result.data, blend = blend.data, i = 0 ; i < data.length ; i+=4)
    {
        if(typeof globalAlpha !== "undefined") gA = globalAlpha * 1;
        else gA = 1;
       
        alpha = blend[i+3] * gA;
        alpha = alpha / 255;

        if(data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 0)
        {
            data[i] = blend[i], data[i+1] = blend[i+1], data[i+2] = blend[i+2], data[i+3] = alpha * 255;
        }
        else
        {
            data[i] = ((data[i]) * (1 - alpha) + -blend[i] * (alpha));
            data[i+1] = ((data[i+1]) * (1 - alpha) + -blend[i+1] * (alpha));
            data[i+2] = ((data[i+2]) * (1 - alpha) + -blend[i+2] * (alpha));
            data[i+3] = 255;
        }
    }
   
    ctx1.putImageData(result, 0, 0);
   
    return src;
}

背景色が白以外の場合に動作がおかしかったのと、
globalAlphaへの対応を追加。
いろいろ計算式をいじっていてこれで動いたけど、
なぜこの計算式なのかは不明。
ChromeとSafariの実装にあわせたからこの式になったけど、
Firefoxと同じ結果になる式のほうが論理的に正しいのかもしれない。