WebDesign Dackel

iOS9で追加されたForceTouch(3DTouch)をJavaScriptで操作してみる

iOS9で追加されたForceTouch(3DTouch)をJavaScriptで操作してみる

Hatena0
Google+0
Pocket0
Feedly0

先週、2年使って画面のバッキバキになったiPhone 5s6sに買い換えました。
買い換えてみて便利だなぁと思ったのは、やっぱりForce Touch(3D Touch)でした!

カーソルの移動や、ブラウザでリンク先をプレビュー出来たりなどなど。

そんな便利なForch TouchJavaScriptから操作する方法はあるのかな?とちょっと調べたら既にやっている方がいました。

こちらを参考にして、操作をしてみたいと思います。

ちなみに2番目の記事にも書かれていたのですが、はじめAppleのドキュメントを見ながらサンプルを作ってみたのですが、期待するイベントがうんともすんとも言いませんでした。

ここらへんの対応は今後のバージョンアップで、ということなんでしょうかね。

タッチの強度を取得する

タッチイベントのイベントオブジェクト内に、forceという値が設定されています。
この中に、0~1までで表されたのタッチの強さが入っています。

具体的には下記のようなコードで取得できます。

document.getElementById("btn").addEventListener("touchstart", function(e){
  console.log(e.touches[0].force);  //0 ~ 1
}, false);

思いの外簡単な操作で取得できるようですね!

とってもシンプルなサンプルを作る

タッチの強さを取得できたので、シンプルなサンプルを作ってみました。
画面中央の円に対するタッチの強度で、影と背景のコントラストに変化が出るシンプルなものです。

シンプルな動作サンプル

動作サンプル

※PCでは動かないので、是非実機でご確認下さい…

サンプルコード

上記のサンプルで使用しているコードはこんな感じです。

HTML/CSS/JS全部一緒に書いてしまったので、少し見づらいですがコピペで動かせるようになってます。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <style type="text/css">
  * {
    margin:0;
    padding:0;
    box-sizing:border-box;
  }

  body {
    overflow:hidden;
    font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
  }

  h1 {
    position:absolute;
    top:30px;
    left:0;
    z-index:1;
    width:100%;
    color:#fff;
    font-size:28px;
    font-weight:lighter;
    text-align:center;
    letter-spacing:.05em;
  }

  #btn {
    position:absolute;
    top:0;
    right:0;
    bottom:0;
    left:0;
    z-index:1;
    width:150px;
    height:150px;
    margin:auto;
    background:#fff;
    border-radius:100%;
    color:#607d8b;
    text-align:center;
    font-weight:lighter;
    line-height:150px;
    letter-spacing:.05em;
  }

  #value {
    position:absolute;
    bottom:30px;
    left:0;
    z-index:1;
    width:100%;
    color:#607d8b;
    text-align:center;
    font-weight:lighter;
  }

  #background {
    position:absolute;
    top:0;
    left:0;
    z-index:0;
    width:100%;
    height:100%;
    background:linear-gradient(to bottom,  #d69af4 0%,#faffcc 100%);
  }
  </style>
</head>
<body>
  <h1>Force Touch Sample</h1>
  <div id="btn">Touch me</div>
  <div id="value"></div>
  <div id="background"></div>

  <script>
  var force = 0,
      touch = null;

  var $btn = document.getElementById("btn"),
      $value = document.getElementById("value"),
      $background = document.getElementById("background");

  // イベントをキャンセル
  function cancelEvent(e){
    e.preventDefault();
    e.stopPropagation();
  }

  // タッチを取得して、値の更新を実行する
  function checkForce(e){
    cancelEvent(e);
    touch = e.touches[0];
    refreshForceValue();
  }

  // タップの強さを更新する
  function refreshForceValue(){
    if( !touch ) return;
    force = touch.force;
    render();
    setTimeout(refreshForceValue, 10);
  }

  // タップの強さを視覚的にフィードバック
  function render(){
    $value.innerHTML = "Force : " + force;
    $btn.style.boxShadow = "0 0 " + ( 10 * force ) + "px rgba(0, 0, 0, " + ( 0.8 * force ) + ")";
    $background.style.webkitFilter = "contrast(" + ( 100 - 40 * force ) + "%)";
  }

  // イベントを設定
  $btn.addEventListener("touchstart", checkForce, false);
  $btn.addEventListener("touchmove", checkForce, false);
  $btn.addEventListener("touchend", function(e){
    cancelEvent(e);
    touch = null;
    force = 0;
    render();
  }, false);

  // 初期化
  render();
  </script>
</body>
</html>

実際のWebアプリにどう活かすか

じゃあこれをどう活かしていくか、っていうところが悩ましい気がします。対応する端末も限られますし。

Force Touchを前提とした機能にするのは微妙な気がするので、PCでいうところの右クリック的な扱いにならざるを得ない気がします。(対応端末が上がってきたら別かもですね!)

サンプルを作ってみた

Force Touchに補助的な操作を任せる、具体的にどんなものがあるかなぁ〜と悩んだので、TODOアプリを想定したサンプルを作ってみました。

TOODアプリの動作サンプル

動作サンプル


動作は下記のような感じです。

  • TODOのタップでチェック/チェック解除
  • テキスト部分のタップで内容を編集
  • TODOForce Touchで次の操作を表示
    • 編集
    • チェック
    • 削除

イベント操作を雑にクラス化

冒頭にも書いたように、Appleのドキュメントに記載された下記のイベントが発行されませんでした。

  • webkitmouseforcewillbegin
  • webkitmouseforcedown
  • webkitmouseforceup
  • webkitmouseforcechanged

Force Touchの開始と終了、一定の強度まで達したかどうかなど、結構重要なイベントですね…。

これらが使えないのは地味に不便なので、サンプルでは下記の様にイベントをラップするようなクラスを作っています。

import {EventEmitter} from "events"
import {cancelEvent} from "../utils/events"


class ForceTouchEvent extends EventEmitter {
  constructor($el, threshold=0.95) {
    super();
    this.$el = $el;
    this.threshold = threshold;
    this.force = 0;
    this.touch = null;
    this.interval = 10;
    this.down = false;
    this.bindEvents();
  }

  bindEvents() {
    this.$el.addEventListener("touchstart", this.handleTouchStart.bind(this), false);
    this.$el.addEventListener("touchmove", this.handleTouchMove.bind(this), false);
    this.$el.addEventListener("touchend", this.handleTouchEnd.bind(this), false);
  }

  unbindEvents() {
    this.$el.removeEventListener("touchstart", this.handleTouchStart.bind(this), false);
    this.$el.removeEventListener("touchmove", this.handleTouchMove.bind(this), false);
    this.$el.removeEventListener("touchend", this.handleTouchEnd.bind(this), false);
  }

  handleTouchStart(e) {
    this.trigger(ForceTouchEvent.WILL_BEGIN);
    this.checkForce(e);
  }

  handleTouchMove(e) {
    this.checkForce(e);
  }

  handleTouchEnd(e) {
    if( !this.down && this.force < this.threshold ){
      this.trigger(ForceTouchEvent.UP);
    }
    this.touch = null;
    this.down = false;
  }

  checkForce(e) {
    if( !e.touches ) return;
    this.touch = e.touches[0];
    this.refreshForceValue();
  }

  refreshForceValue() {
    if( !this.touch ) return;
    let force = this.touch.force;

    // change
    if( this.force != force ){
      this.force = force;
      this.trigger(ForceTouchEvent.CHANGE);
    }

    // down
    if( !this.down && this.force >= this.threshold ){
      this.down = true;
      this.trigger(ForceTouchEvent.DOWN);
      return;
    }

    setTimeout(this.refreshForceValue.bind(this), this.interval);
  }

  trigger(type, ...args) {
    let e = {};
    e.type = type;
    e.timestamp = Math.floor(Date.now() / 1000);
    e.target = this.$el;
    e.force = this.force;
    this.emit(type, e, args);
  }
}

ForceTouchEvent.WILL_BEGIN = "forcetouchwillbegin";
ForceTouchEvent.CHANGE = "forcetouchchange";
ForceTouchEvent.DOWN = "forcetouchdown";
ForceTouchEvent.UP = "forcetouchcup";


export default ForceTouchEvent;

EventEmitterを継承して、Force Touchの始まり/終わり、値の変化を通知するような内容です。

これを次のように使うことで、発行されないイベントを補ってみました。

import ForceTouchEvent from "../events/ForceTouchEvent"

let $el = document.getElementById("hoge");
let forceTouch = new ForceTouchEvent($el);

// Force Touch開始
forceTouch.on(ForceTouchEvent.WILL_BEGIN, e => {
  console.log(e.force); //0 ~ 1
});

// 強度が変更された
forceTouch.on(ForceTouchEvent.CHANGE, e => {
  console.log(e.force); //0 ~ 1
});

// 規程の強度に達した
forceTouch.on(ForceTouchEvent.DOWN, e => {
  // logic
});

// 規程の強度に達しないで、指が離れた
forceTouch.on(ForceTouchEvent.UP, e => {
  // logic
});

サンプルコード全体

はじめ、全体のコードを載せようと思っていたのですが、思っていたよりも長くなってしまったので興味のある方は下記リポジトリよりご確認ください。(雑なコードで見づらいかもしれないです…)

tsuyoshiwada/sample-3d-touch-todo

まとめ

ドキュメント通りのイベントが発行されないのは痛いですが、値の取得自体は簡単なので上手く使えば面白い表現や、UIに活かせそうだなと思います。

実際にサンプルを作ってみて、Force Touchを視覚的にフィードバックを返すのは簡単ですが、それが可能だよという事を分かってもらえる「デザイン」が難しそうだなという気がしました。

これから対応アプリや、Webサイトが増えていくのが楽しみですね!