Humanity

Edit the world by your favorite way

最近読んだ漫画 (と CSS で感想の表示・非表示を切り替えてみるテスト)

あまり人に漫画を勧める時に感想って言いたくない。 けど自分はしょっちゅうブログのデザインを変えるので、単純に背景色と同じにするだけだとテーマを変えた場合に隠したはずの文章が見えてしまう。 それはあまりにもあんまりなので、CSS で感想の表示・非表示を切り替えられるようにしてみた。

案1:シンプルにチェックボックスで切替え

チェックボックスをオンにしてみてください。

はてなブログの場合、設定画面の「デザイン」→「デザインCSS」から追加の CSS を定義できるのでそこに以下の記述を追加。 あと Markdown じゃないと HTML は書けないかも (たぶん)。

label.toggle-opacity > span {
  opacity: 0;
}
label.toggle-opacity > input[type=checkbox]:checked + span {
  opacity: 1;
}

でブログ本文に書くときはこう。

<label class='toggle-opacity'><input type='checkbox'><span>テスト</span></label>

ちょっと書くのが面倒だけどそう使わないだろうし、コピペするなり辞書登録するなりしてしまえばいい。

案2:<details> + <summary>

…でさらにちょっとリッチなボタン形式で表示させてみるかーといろいろ HTML5 のタグを調べてたら、 こういう場合(?)に便利な <details> タグ + <summary> タグというものを発見。こう使う。

<details>
<summary>(感想は省略されました。読むにはここをクリックしてください)</summary>
主人公がかっこよかった(小並)
</details>

で、こうなる。

(感想は省略されました。読むにはここをクリックしてください) 主人公がかっこよかった(小並)

HTML5 のタグなので CSS は一切使ってない。 感想部分の文章にインデントを加えたい場合とかは CSS でごにょごにょすればいいだろうけど、自分はもうこれでいいや。

案3:リッチなボタン風

…と思ってたんだけど、自分が考えてた「リッチなボタン形式」のデザイン案もそれなりに実現してみたかったのでちょっとやってみた (完全にやりたいだけ)。 正直デザインより機能性を求めてしまう自分はもう <summary> タグでいいやって気分になってたけど、 せっかく作ったのでこの記事で使ってみたら結構いい感じだったので良かった。

案としてはこんな感じ。

  • チェックボックスじゃなく代わりに自由なテキストを表示させる
    • テキストは表示時/非表示時で切り替えられる
  • ちょっとリッチなボタン形式で表示

そのデモがこちら。

HTML はこう (長い)。

<label class='toggle-opacity-text'>
  <input type='checkbox'>
  <span class='tot-enable'>感想を読むにはクリック</span>
  <span class='tot-disable'>感想を非表示にする</span>
  <span class='tot-body'>ねーねー本の感想は?</span>
</label>

CSS はこう (長い)。 クラス名はさっきのと変えてます。

label.toggle-opacity-text > .tot-disable,
label.toggle-opacity-text > .tot-body,
label.toggle-opacity-text > input[type=checkbox] {
    display: none;
}

label.toggle-opacity-text > input[type=checkbox]:checked ~ .tot-enable {
    display: none;
}
label.toggle-opacity-text > input[type=checkbox]:checked ~ .tot-disable {
    display: inline;
}
label.toggle-opacity-text > input[type=checkbox]:checked ~ .tot-body {
    display: block;
}

/* ちょっとリッチなボタン風 */
label.toggle-opacity-text > .tot-enable,
label.toggle-opacity-text > .tot-disable {
    color: #fff;
    font-weight: bold;
    background-color: aqua;
    padding: 5px 10px;
    border-radius: 1em;
}

/* ついでに枠もつけてみた */
label.toggle-opacity-text {
    padding: 10px;
    display: inline-block;
    border: 1px solid white;
}

  • (2016/07/18 23:42 追記) 「ボタンのテキストの選択を無効」を追加。
  • (2016/07/19 23:15 追記) スマホで感想が丸見えになっていたのでブログをレスポンシブ化した。
  • (2016/07/19 23:23 追記) Android Chrome でテキスト部分のタップが認識されなくなっていたので、やはり「ボタンのテキストの選択を無効」を削除。削除したコードは以下の通り。
/* ボタンのテキストの選択を無効 */
label.toggle-opacity-text > .tot-disable,
label.toggle-opacity-text > .tot-enable {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

結局 HTML が <span> タグの嵐になってしまった。 さすがにブログの記事で気軽に使えない *1 ので、 それを避けるために適切な HTML タグを探していて偶然 <summary> タグに行き当たった訳だけど、 目的は叶えられたのでとりあえずこれはこれでよしとする。

漫画の感想

という訳でおまけみたいな感じになりつつある漫画の紹介。横道ばかりの人生。

感想だけだと非表示にしてしまったらジャンルも分からなくなるので、 せめてジャンルだけ1行で紹介するようにしてみようかなーと思ったけど、 ジャンルというくくりも微妙にしっくり来ない… ジャンルで興ざめされたり敬遠されてしまうのは悲しいし、 個人的に作品に感じるジャンルやテーマは人それぞれだと思うので、いっそジャンルも隠すようにしてみた。

あとジャンルも各成分の % で表記するようにしたので、「それは違う」ってジャンルが含まれてるのもあるかも。 あと % って言っても適当なのであまり気にしないでください。

なのは洋菓子店のいい仕事

ジャンル ファンタジー(10%) 青春(20%) ラブ(25%) コメ(25%) 菓子(20%)

ねじの人々

ジャンル 青春(10%) ラブ(10%) コメ(30%) 哲学(50%)

いかづち遠く海が鳴る

いかづち遠く海が鳴る(1) (ビッグコミックス)

いかづち遠く海が鳴る(1) (ビッグコミックス)

ジャンル ファンタジー(70%) 恋愛(30%)

ノラガミ

Kindle 版が今0円だった。

ジャンル ファンタジー(40%) 恋愛(30%) 友情(20%)

キレる私をやめたい

ジャンル ノンフィクション(50%) メンタルヘルス(50%) ※ノンフィクション 50% は嘘という訳ではない

さびしすぎてレズ風俗に行きましたレポ

ジャンル ノンフィクション(40%) メンタルヘルス(30%) 性(30%) ※ノンフィクション 40% は嘘という訳では(ry

僕が私になるために

僕が私になるために (モーニングコミックス)

僕が私になるために (モーニングコミックス)

ジャンル ノンフィクション(50%) 性(50%)

MAJOR 2nd

ジャンル 青春(50%) 野球(50%)

週刊少年ガール

週刊少年ガール(1) (週刊少年マガジンコミックス)

週刊少年ガール(1) (週刊少年マガジンコミックス)

ジャンル ファンタジー(34%) 青春(33%) 恋愛(33%)

スペシャ

ジャンル ギャグ(50%) 青春(50%)

彼とカレット。

彼とカレット。<彼とカレット。> (―)

彼とカレット。<彼とカレット。> (―)

ジャンル ギャグ(40%) エロ(ネタ)(30%) かわいい(30%)

ぱら☆いぞ

ジャンル エロ(100%) 頭おかしい(100%) 天才(100%)

AIの遺電子

ジャンル SF(50%) 未来(25%) 友情とか恋愛とか(25%)

木根さんの1人でキネマ

ジャンル ギャグ(40%) 映画(40%) 友情(10%) シェアルーム(10%)

服を着るならこんな風に

ジャンル ファッション(70%) 妹かわいい(20%) こんな妹いるか(10%)

*1:まぁ冒頭で辞書登録すれば良いとは自分でも言ってるけど

*2:たぶんと多分をかけています

*3:この表現が適切だったかどうかはまた読んでみないと分からない

setInterval() を requestAnimationFrame() に変えてもカクカクさせない方法

前提知識

まず requestAnimationFrame() は setInterval() と同じで定期的にコールバックを実行する API

requestAnimationFrame() を使うことによるメリットは以下の通り。

  • タブがバックグラウンドになった時に fps を落として実行される
  • ブラウザの描画更新単位と同じ単位で呼び出される

よって

  • 低メモリ消費
  • 省電力

しかし setInterval() で書いていた処理を requestAnimationFrame() に書き換えるに当たって一つ問題がある。 それは実行する間隔を指定できないこと。 setInterval() は第2引数でミリ秒でコールバックが呼ばれる間隔を指定できる。 しかし requestAnimationFrame() が受け付けるのはコールバックのみ。 ブラウザの描画更新単位と同じ単位で呼び出されるため効率の良い API だが、呼ばれる間隔はまちまちになってしまう問題がある (大体 60 fps と言われているが、バックグラウンドになった際にはもっと低 fps になるらしい)。

コールバックを実行するタイミング指定できない問題をどうにかする

自分は今回糸通しのゲームで requestAnimationFrame() 対応したかった。 このゲームはPCとスマホ両対応といいつつ自分が暇な時に触りたいので圧倒的にスマホの需要のほうが大きい。 しかし FPS を制御できないのはアクションゲームにとっては致命的。 だけど冒頭に挙げたメリットはスマホにとってうれしいことばかりだったのでそれなりに対応したい問題だった。

そこで検索したら以下の記事がヒットした。

requestAnimationFrame のタイミングにたよって値を変えるするというのではなく、経過時間を管理し再描画のタイミングで「経過時間に合わせたフレーム」を表示してやればよさそうです。

requestAnimationFrame でフレームと再描画更新を制御する

上記の記事では「経過時間に合わせたフレーム」を表示するために描画の対象を「コマ割り」し、フレームごとのコマを描画している。 ただ、自分の場合はゲームのメインループをカクつかせずに回す方法が知りたかった。 ゲームはユーザの入力が絡むシーンが大半なので単純にコマ割できず、 毎フレームごとにじりじり座標を更新していかなければならない場合が大半なのでどうすべきか考えた結果、 その方法が割と上手くいったので共有してみる。

解決策

やりたいことは以下のような感じです。

setInterval() を使うなどして FPS を固定させた場合のそれぞれのオブジェクトの移動量を100%とした時、 前回との経過時間から「実際の FPS」を計算し、「想定している FPS」と比較して割合を出します (70%とか120%とか)。 そしてその割合を移動量に掛けることで描画のタイミングがバラついても移動量は FPS 固定の時と同じ移動量になる(はず)なので、 カクつくことなく描画できます。

より分かりやすく実際のアルゴリズムで書いてみると以下の通りです。

  1. 1回の移動量に対する割合を計算する
  2. 描画した時の時間を覚えておく
  3. その計算量に応じてオブジェクトを移動させる

実際のコードはこんな感じです (色々省略してます)。

ito-to-shi/app.js at 65634edaa427a53649f8ac825e71c97b05c36097 · tyru/ito-to-shi · GitHub

  update() {
    // 1. 1回の移動量に対する割合を計算する
    // 注意:この関数では経過時間しか計算していないので嘘コメントです。
    // 実際はそれぞれのオブジェクトで経過時間から割合を計算しています。
    const now = Date.now();
    const elapsedMs = now - this._prevUpdatedTime;

    // 2. 描画した時の時間を覚えておく
    this._prevUpdatedTime = now;

    // 3. その計算量に応じてオブジェクトを移動させる
    // (計算量である elapsedMs をそれぞれの画面のオブジェクトに渡して座標を移動して描画してもらっている)
    screen.update(elapsedMs);
  }

で呼ばれた update() メソッドの冒頭で割合を出しています。

ito-to-shi/running.js at 65634edaa427a53649f8ac825e71c97b05c36097 · tyru/ito-to-shi · GitHub

  update(elapsedMs) {
    const movePercent = elapsedMs / constant.THE_FPS;

    // (省略)
  }

ちなみに 30 FPS の想定ですが、constant.THE_FPS = 1000.0 / 30.0 となっていて非常に紛らわしい変数名になってました… *1

なので上の処理は movePercent = 経過時間(ミリ秒) / 1フレームにかかるミリ秒 になります。 100 ms かかる想定が 200 ms かかったら 2.0 (=200%) です。 その場合は2フレーム分オブジェクトを移動させればいいですね。

失敗例

逆に失敗した方法は、想定している FPS 分の経過時間が経っていなかったら座表計算や描画をスキップする方法です。 これだとスキップした時には当然描画は行われないため、カクついてしまいます (この件で描画全体をスキップするのではなく毎回描画は行う方法でないとカクつくんじゃないかと思って冒頭の方法に変えた)。

ito-to-shi/app.js at f29fae827b02d238ca753ff20f60ad69cbfb3a6e · tyru/ito-to-shi · GitHub

  update() {
    // Skip if main loop was called too early.
    const now = Date.now();
    const stepFrames = Math.floor((now - this._prevUpdatedTime) / constant.THE_FPS);
    if (stepFrames > 0) {
      this._prevUpdatedTime = now;
    }
    // Update screen.
    const dispatcher = this._screenDispatcher;
    const screen = dispatcher.screens[dispatcher.screenId];
    if (screen && screen.update) {
      screen.update(stepFrames);
    }
  }

stepFrames > 0 の時のみ「描画した時の時間を保存&呼ばれた update() メソッドで描画」しているため、小数点以下が考慮されていません。

呼ばれた update() メソッドは以下の通り。

ito-to-shi/running.js at f29fae827b02d238ca753ff20f60ad69cbfb3a6e · tyru/ito-to-shi · GitHub

  update(stepFrames) {
    for (let i = 0; i < stepFrames; ++i)
      if (!this._doUpdate())
        break;
  }

前回の経過時間から描画するフレーム数ごとに描画関数を呼ぶようにしただけ。 名前は変わってますが movePercent と stepFrames は同じ値ですね。 この方法だとstepFrames=0.7(70%)とかの場合、ループは1回も回りません。

実際のデモ

(この記事のデモのつもりじゃないけど) 実際に以下のリンクから遊べます。

一応前のバージョンと比較してプレイしてみたかったので、前のバージョンもプレイ可能にしてみた。

前のバージョンと比べると普段の操作はむしろ前よりもヌルヌルになった気がします (ブラウザからすると setInterval() の方が非効率な API なんでしょうね…)。 ちなみに前のバージョンは色々バグっていたのでそこはあまりツッコまないでください。

*1:今は修正済み。あとついでに movePercent もダサい名前で変えたい…けど良い名前が思いつかない

現代的な React の書き方へのリンク

React 初心者を対象とした記事です。昨日の記事からの続き。

ES6版React.jsチュートリアル - Qiita もオススメ。

差分で見る現代化のようす

Rewrite in ES6 style · tyru/hello-react@22d4e7e · GitHub

-const React = require('react');
-const ReactDOM = require('react-dom');
+import React from 'react'
+import ReactDOM from 'react-dom'
 
-const Hello = React.createClass({
-  render: function() {
+class App extends React.Component {
+  render() {
     return (
-      <div className="container">Hello {this.props.name}</div>
+      <div className="container">Hello React in ES6 style</div>
     );
   }
-})
+}
 
-ReactDOM.render(<Hello name="React" />, document.getElementById("app"));
+ReactDOM.render(
+  <App />,
+  document.getElementById("app")
+);