ちら帳

喉元を過ぎると熱さを忘れる自分の為の、ちら裏メモ帳ブログです。

React VR : 注視カーソルでクリックする

React&Three.js初心者のため、週一で記事を書くのがそろそろ厳しくなってきました。ちらです。

今回はVR界隈ではちょくちょく見かける、見つめているとクリックできるボタンを作りました。

作ったボタン

ボタンを見つめている間、何も起きないと面白くないので、次のような効果もつけました。

  • ボタンを見つめているかどうかでボタンのテキストが変わる
  • ボタンを見つめている間はボタンがぶるぶるする
  • 物理クリック or 注視クリックされたらボタンのテキストが変わってクリック音もする

結果、できたものがこちらです(GIFを撮る都合上、VRモードにはしていません) f:id:chira_pym:20170612222738g:plain

こうして見てみると、もっとまともな例を作るべきだった気がしてなりませんが、寝ている顔文字をぶるぶるさせるのも、クリック時にお布団から飛び出るようにするのも実装しているときは結構楽しかったです。

実装

カーソルの表示

注視入力を実装するにあたりカーソルを表示させる必要があります。

詳しい説明については、以前記事に書いたので割愛します。 chira-memo.hatenablog.com

注視クリックできるボタンの実装

注視でボタンをクリックできるようなAPIはないので、onEnterの発火時にsetTimeoutを使って、クリックしたときに実行したい処理を1秒後に実行するようにします。
視線がボタンから外れたり、入力機器を使ってボタンがクリックされたときにはclearTimeoutでタイマーを解除します。

今は自力で実装する必要がありますが、UI的にはよくあるパターンのひとつなので、そのうち標準APIになったりするのかもしれません。

サウンドの再生についてはこちらを参考にしました。

facebook.github.io

アニメーションは…ドキュメントを読んでもあまりよく分からなかったので、参考になりそうなコードを見つけてきて見様見真似で書きました。変な書き方になってたらごめんなさい。

そのうちアニメーションについても記事が書けるといいなと思います。

"use strict";

import React from "react";
import {
    AppRegistry,
    asset,
    Pano,
    Text,
    Image,
    View,
    VrButton,
    Animated,
    VrSoundEffects
} from "react-vr";

import CylindricalPanel from "CylindricalPanel";

const Easing = require("Easing");

class ReactVrSample extends React.Component {
    render() {
        return (
            <View>
                <Pano source={asset("chess-world.jpg")} />
                <CylindricalPanel
                    layer={{ width: 2000, height: 720 }}
                    style={{ position: "absolute" }}
                >
                    <View
                        style={{
                            opacity: 1,
                            width: 2000,
                            height: 720,
                            alignItems: "center",
                            justifyContent: "center",
                            marginTop: 100
                        }}
                    >
                        <SleepingButton />
                    </View>
                </CylindricalPanel>
            </View>
        );
    }
}

class SleepingButton extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            text: "(¦3[___]",
            shakeValue: new Animated.ValueXY({ x: 0, y: 0 })
        };

        this.clicked = this.clicked.bind(this);

        VrSoundEffects.load({
            wav: asset("click.wav"),
            ogg: asset("click.ogg"),
            mp3: asset("click.mp3")
        });
    }

    render() {
        return (
            <Animated.View
                style={{
                    transform: this.state.shakeValue.getTranslateTransform()
                }}
            >
                <VrButton
                    onEnter={() => this.enter()}
                    onExit={() => this.exit()}
                    onClick={() => this.click()}
                >
                    <Text
                        style={{
                            margin: 10,
                            width: 400,
                            fontSize: 70,
                            fontWeight: "300",
                            borderRadius: 20,
                            backgroundColor: "pink",
                            textAlign: "center"
                        }}
                    >
                        {this.state.text}
                    </Text>
                </VrButton>
            </Animated.View>
        );
    }

    enter() {
        this.timeout = window.setTimeout(this.clicked, 1000);
        this.setState({ text: "(:3[___]" });

        const shakeValue = 1;

        this.animation = Animated.parallel([
            this.shake(shakeValue, shakeValue)
        ]);

        this.animation.start();
    }

    exit() {
        this.timerReset();
        this.clearAnimation();
        this.setState({ text: "(¦3[___]" });
    }

    click() {
        this.timerReset();
        this.clicked();
    }

    clicked() {
        this.clearAnimation();
        this.setState({ text: "(:з )=[___]" });
        VrSoundEffects.play({
            wav: asset("click.wav"),
            ogg: asset("click.ogg"),
            mp3: asset("click.mp3")
        });
    }

    timerReset() {
        if (!this.timeout) {
            return;
        }

        window.clearTimeout(this.timeout);
        this.timeout = null;
    }

    clearAnimation() {
        if (!this.animation) {
            return;
        }

        this.animation.stop();
        this.animation = null;
    }

    shake(x, y) {
        return Animated.sequence([
            Animated.timing(this.state.shakeValue, {
                duration: 30,
                toValue: { x, y },
                easing: Easing.linear
            }),
            Animated.spring(this.state.shakeValue, {
                toValue: { x: 0, y: 0 },
                friction: 0.5,
                tension: 400
            })
        ]);
    }
}

AppRegistry.registerComponent("ReactVrSample", () => ReactVrSample);

ボタンじゃなくてカーソルの方をアニメーションしたい

できればカーソルをリング型にして、進捗状況が分かるようなプログレスリングにしたいところなのですが、 カーソルの描画部分はライブラリの方にハードコードされてしまっているので難しいのが現状です。

github.com

makeDefaultCursorメソッド内にこんな感じのコメントがついているのでそのうちカスタマイズできるようになるかもしれません。

// TODO: Customize color or change color on hit.
// TODO: Support image provided by us or by user.

自分はおとなしくアップデートを待ちたいと思います(´・ω・`)

今日のひとこと

JSXのインデントのつけ方が全然分からないのでJsPreitterをSublime Textに入れました。
プラグインなしでは生きていけない体になりつつあります。