anti scroll

ブラウザと小説の新しい関係を模索する

ゲームパッドの操作を記述する言語「combo-script」と、その再生アプリ「combo-player」を作りました

きっかけ

作ったきっかけは、最近買ったウイイレ2020の操作が、ちっとも頭に入らなかったからです。。。

一応ゲーム内にマニュアルがあるんですけど、文字とか矢印で説明されても、ピンと来ないんですよね。。。

そこで思いついたのが「ゲームの操作をぱぱっと記述したら、パッド上の動きをアニメーションを再生しつつ、操作ログも出してくれる」みたいなものがあったら、理解が捗るかなあ〜?ということです。

というわけで作ったのが、combo-scriptと、combo-playerです。

例えばcombo-scriptで昇龍拳を記述して、combo-playerで再生するとこんな感じになります。

f:id:convertical:20200811165349g:plain
昇龍拳

その他、色々なコマンドについては、デモサイトを見てください。

tategakibunko.github.io

上のデモサイトでは、ウイイレの他に、スト5とか鉄拳のコマンドとかも追加しておきました。

メニューの「Games」から選択できます。

コマンド部分は編集可能になっているので、色々と書き換えて遊んでみてください。

f:id:convertical:20200811165922p:plain
コマンド部分(編集可能)

combo-script

combo-scriptは、ゲーム操作を記述するための言語です。こんな風に記述します。

コナミコマンド

up, up, down, down, left, right, left, right, A, B

昇龍拳十字キー操作で記述)

right, down, (right, down), punch

(right, down)は右と左を同時押し(つまり右下)という意味です。

一般にボタンの同時押しは(X, Y, Z...)のように記述します。

昇龍拳(スティック操作で記述)

setL(0), moveL(0, 90), rotateL(90, 45), punch

これは左スティックを0度(真右)に方向け、そこから真下に動かし、更にそこから45度(右斜め下)の位置まで回転させ、最後にパンチボタンを押す、という意味です。

setL, moveL, rotateLの引数は、傾ける先の角度ですが、この角度は時計回りであることに注意してください。時計回りなので、90度は真上ではなく、真下です。

ウイイレ、コントロールシュート

R2 { square }

上の構文はHolding Syntaxと言って、X { Y, Z, ... } などと書くと「Xの操作をしながらY,Z」という意味になります。

なので上のコマンドは

  • R2を押しながら□ボタン
  • 終わったらR2を離す

という意味になります。

combo-script(言語仕様)

詳しくは公式サイトを見てほしいのですが、大雑把に説明すると、

  • ボタンの押下、押しっぱなし、同時押し、各種スティック操作などを抽象化した言語です。
  • 特別なプログラミング知識がいらないので、誰でもコマンド部分を記述できます。
  • 矛盾した操作はコンパイル時にエラーが出ます(押し込んでいる最中のボタンを再び押すとか、左と右を同時に押すとか、色々)。
  • 押下についてはpushDown, pushUpを個別に扱う方法もあります。
  • プラグインで拡張することができます。
  • 定義済みの関数(rotateLとかmoveLとか色々)以外のシンボル名は、ユーザー定義のボタン名、もしくはユーザー定義のプラグイン関数として解釈されます。
  • 例えばpunchと書いたら、ユーザー定義のpunchボタンがあって、それを押して離す、という一連の動作として解釈されます。つまり、punchという名前のボタンをどう解釈するかは、combo-scriptを組み込む実装者に任されます。
  • 関数も同様です。例えばfoo(1,2)と書いた場合、そういう定義済みの関数はないので、ユーザー定義のプラグイン関数とみなされます。そして、実際の動作はcombo-scriptを組み込む実装者に任されます。
  • つまりボタン名も関数名も、それを解釈するのはプレイヤー側の仕事で、combo-script側は名前にルールや制限を設けません。コンパイラ側が判断するのは、それがボタンか、関数か、そしてそれらの動作は、どういう文脈で押されたか、ということだけです(通常押し、同時押し、押しっぱなしなど)。
  • というわけで、例えばパッド右側にある4つのボタンなどは、プレステ風にtriangle/circle/cross/squareと定義してもいいし、Xbox風にY,B,A,Xと定義してもいいです。
  • ただしボタン名を数字から始めることはできません。つまりbutton1はいいけど、1buttonは駄目。
  • ちなみに、combo-scriptは大文字と小文字を区別しません。だからupと書いてもUPと書いても同じです。これは関数も同じ。
  • あとボタン名については英数じゃなくてもいいです。例えば日本語で強パンチとか書いても問題ありません。

コンパイルの仕方とか、それをどうやって自分のプレイヤーアプリに組み込むか、みたいなことは公式サイトを確認してください。

感想

  • ウイイレ2020は難しい。
  • 昇龍拳のコマンドを初めて知った。
  • 鉄拳のコマンドを思い出すことが出来た。

先頭ページに「登場人物の一覧」が表示されるようになりました

作品に登場人物を登録した場合、先頭ページに「登場人物の一覧」が表示されるようになりました。

f:id:convertical:20200705102918p:plain
登場人物の一覧

市販の書籍でも、だいたいこんな感じで冒頭に表示されてますよね。

登場人物の挿絵については、小説本文の下側に「登場人物」のタブメニューがあって、そこをクリックすると挿絵付きで一覧が表示されます。

f:id:convertical:20200705102945p:plain
挿絵付きの登場人物一覧

人物をクリックすると、こんな感じで詳細情報が表示されます。

f:id:convertical:20200705103223p:plain
人物の詳細

jingoo 1.4.0 をリリースしました

jingoo 1.4.0をリリースしました。

github.com

新しくなった点

  • Jg_templateモジュールを部分的に改良したJg_template2というモジュールが追加されました。

サンプル

これまでJg_templateモジュールは、次のようにmodels連想リストを与えていました。

open Jingoo
open Jg_types

let result = Jg_template.from_string "{{ msg }}" ~models:[
 ("msg", Tstr "hello, world!");
]

Jg_template2モジュールからは、models名前を与えたら値を返す関数を渡します。

open Jingoo
open Jg_types

let alist = [("msg", Tstr "hello, world!")]
let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key ->
  try List.assoc key alist with Not_found -> Tnull
)

これの何が良いのかというと、string -> tvalueという型の関数にさえなっていれば、データソースは何でも良くなることです。

例えばデータソースを(連想配列より高速な)Hashtblにすることもできます。

open Jingoo
open Jg_types

let htbl = Hashtbl.create 1 in
let () = Hashtbl.add htbl "msg" (Tstr "hello, world!") in
let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key ->
  try Hashtbl.find htbl key with Not_found -> Tnull
)

データソースなしで、単なる関数にしてしまうと、さらに高速です。

open Jingoo
open Jg_types

let result = Jg_template2.from_string "{{ msg }}" ~models:(function
 | "msg" -> Tstr "hello, world!"
 | _ -> Tnull
)

関数にすることで、乱数値みたいなものを扱うこともできます。

open Jingoo
open Jg_types

let result = Jg_template2.from_string "{{ msg }}, {{ rand }}" ~models:(function
 | "msg" -> Tstr "hello, world!"
 | "rand" -> Tint (Random.int 100) (* 0 ~ 100 の値をランダムに返す *)
 | _ -> Tnull
)

将来的にはこっちをメインのAPIとして使用したいのですが、旧版のAPIで既に広く使用されてしまっているので、今回はモジュールを別名で分けることで対応しました。

参考

In Jingoo.Jg_template, provide a function for the getting variables · Issue #112 · tategakibunko/jingoo · GitHub

シリーズ作品において続きの作品へのリンクが本文の最後に表示されるようになりました

Togetterの次の記事に「探す作業が嫌だ」みたいなことが書かれていて「そりゃごもっとも…」と思ったので、表題の件を実装しました。

少年ジャンプ+副編集長が大学1年生から「漫画アプリのUIについて物申したい!」というDMが来て実際に会って考えが整理されて意義深い時間になったという話 - Togetter

これまでは、シリーズを設定している作品について、続きを読む際に、いったんシリーズ作品のリンクへ飛んでから、自分で続きを探さないといけませんでした。

これからは続きがある場合は、本文の最後にリンクが表示されます。

f:id:convertical:20200615125638p:plain
続きがある場合は、リンクを表示する

当たり前にそうあるべきことが、これまで出来ていなかったことが、ちょっと恥ずかしいです。

nehan7をリリース

nehan6からnehan7にバージョンアップしました。

インストール

npm install --save nehan

変更点

  • 組版速度が約20%向上しました。
  • コードサイズが約10%削減されました。
  • nehan.cssが不要になりました。
  • 行内に置換要素、画像、ルビ、圏点傍点、複数サイズの文字などを同時に含むような場合でも、ベースラインが正確に計算されるようになりました。
  • テーブルセルに対してvertical-align: middleが効くようになりました。
  • いくつかのパターンにおける文字詰めの表示崩れを修正しました。
  • 行末揃えの処理を指定しても組版速度が落ちなくなりました。
  • PageReaderは廃止されました。今後はPagedHtmlDocumentを使用してください(後述)。

縦書き文庫のビューアーの刷新

縦書き文庫のビューアーも、新バージョンのエンジンに差し替えました。

nehanのコードサイズが減ったぶん、ページの読み込みも(少しだけ)速くなったと思います。

500KByteほど削減されているので、モバイル環境なんかでは、そこそこ効果が大きいのではないでしょうか。

UIについても、ちょっとだけ刷新しました。

これまでは「本文」と「目次・登場人物」の2列で表示していたのですが、今後は本文を一列(ワンカラム)で表示するようにして、「目次・登場人物」は本文下のメニューに移動させました。

これにより、本文は表示領域が広くなり見やすくなった一方、目次や登場人物に対する操作性は悪くなってしまったので、これに関しては今後の課題としておきます。

(本文の横に目次やら人物やらのタブを表示したらいいのかなあ、と考えています)

nehan7のざっくりとした使い方(開発者向け)

だいたいこんな感じで使います。

import { PagedHtmlDocument, CssStyleSheet } from 'nehan';

const src = "<h1>hello, nehan!</h1>";
const doc = new PagedHtmlDocument(src, {
  styleSheets: [
    new CssStyleSheet({
      "body": {
        "writing-mode": "horizontal-tb", // or "vertical-rl"
        // インライン方向のサイズ(横書きならwidth、縦書きならheightに相当)
        "measure": "450px", 
        // ブロック方向のサイズ(横書きならheight、縦書きならwidthに相当)
        "extent": "500px",
        "font-size": "16px",
        "padding": "1em"
      }
    })
  ]
});
const $dst = document.querySelector("#dst"); // 結果を格納するDOM

// 描画を開始(非同期処理)
doc.render({
  onPage: (ctx){
    const page = ctx.caller.getPage(ctx.page.index); // ページを評価
    $dst.appendChild(page.dom); // 評価したページのDOMを注入
    $dst.appendChild(document.createElement("hr")); // 一応、分割線
  },
  onComplete: (ctx){
    console.log(`終わりました(${ctx.time}msec)`);
  }
});

注意事項1(論理プロパティについて)

nehanは論理組版エンジンなので、領域サイズの指定にwidthとかheightなどは使えません。

代わりにmeasureとかextentという論理プロパティで指定します。

measureは横書き(writing-mode:"horizontal-tb")ではwidthのことです。

縦書き(writing-mode:"vertical-rl")ならheightになります。

extentは横書き(writing-mode:"horizontal-tb")ではheightのことです。

縦書き(writing-mode:"vertical-rl")ならwidthになります。

同じく、top, right, bottom, leftといった名前は、nehanではそれぞれbefore, end, after, startと指定します。

例えば横書きのmargin-leftは、nehanではmargin-startです。

before/afterはブロック方向の前後で、start/endはインライン方向の前後、と覚えてください。

注意事項2(段組とページ送りについて)

上のサンプルでは、appendChildを使って、組版結果をどんどん追記する「段組み方式」で表示していますが、ページ送りで表示したいときは、自前で現在表示するページやページ番号などを管理する必要があります。

その場合、起動時には先頭ページだけを表示し、それ以降のページはページ送りをしたタイミングでdoc.getPageを使って動的に対象ページを取得し、画面上のDOMを差し替える、という実装になるでしょう。

ページ送りの場合の実装については、サンプルのbook.tsなどが参考になるかもしれません。

Angular(<=8)でnehanを使っている方への注意

結論だけ先に述べておきます。

  • Angular8以下では、nehan(<=6.0.38)までしか使えません。
  • ただしTypescript3.6.3以降を使える環境では、なんの問題もなくnehan(>=6.0.40)を使用できます。

以下に理由を書きますが、非常にしょうもないことです。

package.jsonのミス

nehan(<=6.0.38)においては、package.jsontypesという属性の値が./dist/indx.d.tsとなっていました。

しかし実はこれ正しくは./dist/index.d.tsと書かねばならなかったのです(eの字が抜けていた)。

- "types": "./dist/indx.d.ts",  // version 6.0.38
+ "types": "./dist/index.d.ts", // version 6.0.40

先に「Angular8以下では、nehan6.0.38を使ってください」と書きましたが、つまりそれは、このバグったpackage.jsonを使ったversion6.0.38を使ってください、ということです。なぜか?

理由は「そのバグがあるために、Typescript3.5以下でもエラーが出ないから」です。つまりバグのある古いバージョンを使うことで、Typescript3.5以下しか使えないAngular(<=8)でバグがでない、ということです。

実はnehanは各所にgetterと言われる機能を使っているのですが、実はこれ、Typescript3.6.3以降じゃないと次のようなエラーが出て、正しく使えなかった機能だったのです。

error TS1086: An accessor cannot be declared in an ambient context.

でもnehan6.0.38は正しいindex.d.tsが読み込めていない状態だったので、幸運にも? Typescript(<=3.5)でもこのエラーが出なかったのです!

というわけで、Typescript3.5.3までしか使えないAngular8では、nehan(>=6.0.40)を使用できません…

ちなみにAngular9になると、Typescript3.6系列がサポートされるらしいので、そこからはnehanを6.0.40にアップデートできます。

TypeNovelのVSCode拡張を公開しました

VSCodeのマーケットプレースにvscode-typenovelというTypeNovel用のVisual Studio Code拡張を公開しました。

marketplace.visualstudio.com

インストール後は、*.tnファイルを編集するときに有効になります。

実際に動かすと、こんな感じです。

https://raw.githubusercontent.com/tategakibunko/vscode-typenovel/master/images/capture.gif

主な機能

  • ブロック(@で始まる)と注釈($で始まる)のインテリセンス
  • 組み込みマークアップの引数説明を表示($rubyとか@notesとか)
  • 文法のハイライトや、括弧を自動で閉じる設定
  • セーブ時にエラーをチェック(注釈されていない制約、未定義の制約、重複した制約など)

ちなみに$で起動するインテリセンスは、もちろん組み込みタグだけではなく、ブロックで定義した制約の一覧も表示します。

連絡先

バグや機能要望はGithubにて受け付けています。

github.com

余談(開発でハマったところ)

1 .vscodeignoreファイルの挙動

vsce packageコマンドで拡張機能をパッケージ化するときに、.vscodeignoreファイルでパッケージに含めないファイルを定義できるのですが、このファイルに.gitignoreを入れると、.gitignoreの中身を勝手に見て、その中に記述されたファイルもパッケージの対象外にしてしまうみたいです。

つまり.gitignoreファイルに「gitには必要ないがvs拡張のパッケージには含めたい」ファイルなんかを記述してしまったときは、必要なファイルがパッケージ化されなくて、うまく拡張機能が動作しません。

これを回避するのは簡単で、ようするに.vscodeignore.gitignoreを記述しなければよいのです(ちなみに.gitignoreは、.vscodeignoreに記述しなくてもパッケージ対象にはならない模様)。

ちなみにパッケージされるファイルを事前に確認したい場合は、vsce packageをする前にvsce lsとすると、パッケージされるファイル一覧を事前に確認することができます。

2 ローカルの.vsixファイルをVSCodeのUIから直インストール・アンインストールした際の挙動

あと、ローカルでインストールした.vsixファイルを、VSCodeのUIからアンインストールした場合、.vscode/extensions/<拡張機能のフォルダ>が削除されないバグがあるみたいです。

で、わかりにくいことに、この状態でVSCodeのUIから新しい.vsixファイルを読み込ませて上書きインストールさせようとすると、インストールは成功と表示されるのですが、実際には何も新しいファイルが展開されず、古いディレクトリだけが残っている状態になってしまいます。

つまりバグを修正して再インストールしても、修正されていない古い拡張が残り続けるので、まったく修正されていない状態になっているわけです。

この状態について、最初は「修正が正しくない」と判断して、ハマってしまいました。そこから「実は再インストール自体がなされておらず、古い拡張が残り続けているだけ」と気付くのに小一時間ぐらいかかりました。

ようするに「ローカルでインストール・アンインストールのテストするときは、UIを介さずに手動で拡張機能ディレクトリを削除する必要がある」ということになります。

jingoo v1.3.1をリリース

久しぶりになりますが、jingooのv1.3.1をリリースしました。

Release v1.3.1 · tategakibunko/jingoo · GitHub

変更点

  • 演算子として、新たに+=, -=,*=, /=,%= がサポートされました。
  • 匿名関数がサポートされました。
  • 条件分岐の構文にて、elseif だけではなく、else ifを使うこともできるようになりました。
  • macro ~ endmacro構文にて、endmacroの後に、macro名を付け足すことができるようになりました。

匿名関数はこんなふうに使います。

{# 2と表示される #}
{{ ((i) => i + 1)(1) }}

最後のmacro~endmacroについては、こんな風に使います。

{% macro li (content) %}<li>{{content}}</li>{% endmacro %}

{# v1.3.1からは、こういうふうにも書ける #}
{% macro li (content) %}<li>{{content}}</li>{% endmacro li %}

マクロの中身が長くなったときに「あれ、これってなんのマクロの宣言だっけ?」とかならないように、endmacroの部分で、メモ代わりみたいな感覚でマクロ名を付け足すこともできるようになった、ということです。

ただし次のように、宣言したマクロ名と違う名前を付け足すと、構文エラーになります。

{# エラー(マクロ名はliであって、fooではない) #}
{% macro li (content) %}<li>{{content}}</li>{% endmacro foo %}