備忘録: Three.jsで軸方向を変える(注意点あり)

Three.jsでは座標軸の垂直方向がBlenderのz軸とは異なりy軸になっています。
それに伴い、LookAtメソッドなどで視点を変えるとy軸を垂直方向として回転などは計算されます。
これは

THREE.Object3D.DEFAULT_UP = new THREE.Vector3(0, 0, 1);

などをしてObject3D.DEFAULT_UPプロパティの値を変えればだいたいのオブジェクトについては動作します。
ただし、Three.js派生のアドオンなど(FirstPersonControlsとか)は内部的にy軸を垂直方向とした計算をしているようなので、特段の事情がない限りはy軸を垂直方向として各種設定をするのが無難のようです。

これを調べるに至ったのは、過去のFirstPersonControlsにてlon, latプロパティを設定して見ている方向変えられるようだったのですが、いつの間にか廃止されていたためコード読んで色々調べたところ、カメラ方向計算がLookAtメソッド依存、ただそのほかにも操作系のコード見るとポインタ位置に対する操作軸がどうにも固定そうだったのでこのような結論に至りました。

初めてのThree.jsを読む。あとドーナツ大回転。

読んだもの

www.oreilly.co.jp

読んでみて

使いどころが難しいですね。映像作品作るならBlenderなりで直接作ればいいわけであって、Webで遊べるゲーム作りたいならUnityなりでいいんじゃないかという気もします。
動的にコンテンツをロードして連携するとか、そういうあたりになるのでしょうか。それでもインタラクティブに動かすというのは結構難しそうな印象。作ろうかなと思っているものはありますけどね。
ひとまずこれを読んでおけばだいたいのものは作れるんじゃないかなーと思います。

ドーナツ大回転

Blenderの某チュートリアルでおなじみのドーナツを回転させるやつをせっかくなのでThree.jsで作ってみました。
これ。
rutilicus.github.io

Google スプレッドシートでYouTubeの埋め込みプレーヤーを表示させる。でもあまり使えない。

まとめ

  • Google Apps Scriptを使ってスプレッドシートのサイドバーにYouTubeの埋め込みプレーヤーを表示させるよ!
  • でもGoogle Apps Scriptの実行は編集権限がないとできないよ!
  • つまり閲覧権限だけしかない場合は実行できないよ!

実行イメージ

www.youtube.com
ちなみにサーモン爆破は10周年らしいです。マジか……

やったこと

以下の2ファイルを作成。

code.gs

function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('Player').addItem('Show Player', 'showPlayer').addToUi();
}

function showPlayer() {
  const ui = SpreadsheetApp.getUi();
  const html = HtmlService.createTemplateFromFile('player').evaluate();
  html.setTitle('Player');
  ui.showSidebar(html);
}

function getCurrentRowData() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const range = sheet.getActiveCell();
  const id = sheet.getRange(range.getRow(), 1).getValue();
  const start = sheet.getRange(range.getRow(), 3).getValue();
  return {'id': id, 'start': start};
}

player.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <div id="ytplayer"></div>
    <div>
      <input type="button" value="Start"
        onClick="google.script.run.withSuccessHandler(playVideo).getCurrentRowData()">
    </div>
    <script>
      var tag = document.createElement('script');

      tag.src = "https://www.youtube.com/iframe_api";
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

      var player;
      function onYouTubeIframeAPIReady() {
        player = new YT.Player('ytplayer', {
          height: '200',
          width: '200',
          videoId: '',
          events: {}
        });
      }

      function playVideo(videoInfo) {
        player.loadVideoById({'videoId': videoInfo.id,
                              'startSeconds': videoInfo.start});
      }
    </script>
  </body>
</html>

これを拡張機能のApps Scriptで設定後、再度スプレッドシートを開くとメニューバーに「Player」が表示、その中の「Show Player」を押下するとサイドバーにYouTubeの埋め込みプレーヤーが表示されます。
サイドバーの「Start」ボタンを押下すると、スプレッドシート中で現在選択中の行の情報で再生を開始します。

問題点

  • サイドバーの幅は300px固定のため、映像は見えないものと思ってください。音を聞く用途なら使えると思います。最初これやろうと思ったのもその用途が目的でした。
  • Apps Scriptが編集権限がないと実行できないらしく、閲覧権限のみでスプレッドシートを開いてもメニューバーに「Player」が表示されません。

結論

GitHub - rutilicus/youtube-custom-music-playerのいい代替手段にはならなかった。(ダイマ)

読んだ: 曖昧性とのたたかい / 曖昧性との共存

www.shoeisha.co.jp
www.shoeisha.co.jp

システム構築は常に曖昧性が伴い、仕様が膨張するものではあるが、だからと言って曖昧な状態であるまま先延ばししない、先を予見して起こるかもしれない事象に先手を打って対処する……という内容の本であったと思う。
受注段階から関係するマネージャ向けの要素が多いが、その属性に該当しない私にも役に立つ部分が多かったように思う。目下のところ新人の育成どうするか……目立ちやすいところをあえてやらせるとあったけど本当にやらせてよいものか悩む。

AtomフィードをCSVのテンプレートから作るWebアプリをゆるっと作ってみる。

導入

Twitter凍結騒ぎで一部で話題になった(?)RSSフィードAtomフィード。仕様を知らなかったのでAtom(特に配信フォーマット)を調べてみるついでに作る方もゆるっと実装してみました。

今回調べるにあたってRFC 4287 The Atom Syndication Format 日本語訳を拝見しました。この場を借りてお礼申し上げます。

つくるもの

出力内容文法

Atom配信フォーマットのサブセットをゆるっと定義します。厳密さはほぼほぼ考えていません。ゆるっと。
 \langle\mathit{atom}\rangle::=\langle\mathit{xmldecl}\rangle\langle\mathit{feed}\rangle
 \langle\mathit{xmldecl}\rangle::={\tt < ?xml\ version="1.0"\ encoding="utf-8"? >}
 \langle\mathit{feed}\rangle::={\tt < feed\ xmlns="http:// www .w3.org/2005/Atom" >}\langle\mathit{feedmeta}\rangle\langle\mathit{entries}\rangle{\tt < /feed >}
 \langle\mathit{feedmata}\rangle::=\langle\mathit{author}\rangle\langle\mathit{id}\rangle\langle\mathit{link}\rangle\langle\mathit{title}\rangle\langle\mathit{updated}\rangle
 \langle\mathit{author}\rangle::={\tt < author >< name >}\langle\mathit{string}\rangle{\tt < /name > < /author >}
 \langle\mathit{entries}\rangle::=\langle\mathit{entry}\rangle | \langle\mathit{entry}\rangle\langle\mathit{entries}\rangle
 \langle\mathit{entry}\rangle::={\tt < entry >}\langle\mathit{id}\rangle\langle\mathit{link}\rangle\langle\mathit{title}\rangle\langle\mathit{updated}\rangle\langle\mathit{summary}\rangle{\tt < /entry >}
 \langle\mathit{summary}\rangle::={\tt < summary\ type="}\langle\mathit{type}\rangle{\tt " >}\langle\mathit{string}\rangle{\tt < /summary >}
 \langle\mathit{id}\rangle::={\tt < id >}\langle\mathit{url}\rangle{\tt < /id >}
 \langle\mathit{link}\rangle::={\tt < link\ href="}\langle\mathit{url}\rangle{\tt "/>}
 \langle\mathit{title}\rangle::={\tt < title >}\langle\mathit{string}\rangle{\tt < /title >}
 \langle\mathit{updated}\rangle::= {\tt < updated >}\langle\mathit{date}\rangle{\tt < /updated >}
 \langle\mathit{type}\rangle::= {\tt text} | {\tt html}
 \langle\mathit{string}\rangle::=\mathrm{任意のエスケープ済文字列}
 \langle\mathit{url}\rangle::=\mathrm{URL文字列}
 \langle\mathit{date}\rangle::=\mathrm{YYYY-MM-DDThh:mm:ss+09:00形式の日付}
本来IDはリソースが移転しても同一であることが求められているためURL文字列をそのままIDとするのは不適切ですが……ゆるっと作るためここは無視します。

ユーザ入力検討

文法定義より、feed部の入力要素はauthor, URL(link), title, 日付(updated)の4要素、各entry部の入力要素はURL(link), title, 日付(updated), summary, type(summary)の5要素となりますが、feed部のupdatedはentry部の最新のupdatedをそのまま採用するものとしてユーザ入力はなしとします。

入出力定義

feed部のcsvとentry部のcsvの2入力とし、作成したAtomフィード(.atomファイル)を出力とします。
feed部のcsvはauthor, title, URLの3列からなる1行のcsvファイル、entry部のcsvはtitle, type, summary, URL, updatedの5列からなるn行(n > 0)のcsvファイルとします。

つくった

github.com

読んだ: ソフトウェアシステムアーキテクチャ構築の原理 第2版

書籍情報

www.sbcr.jp

感想

正直1割も理解できていない気がするのでざっと感想だけ……
システムアーキテクトのやることはだいたい以下の感じ。

なお、本書は主にアーキテクチャの作成に関する指針を書いたものですが、数百ページあります。

うーん……超人かな? コミュニケーション能力が死んでいる私が目指す道ではないですね。

Roomを使って格納するデータとViewModel連携のあれこれ。

つくりたいもの

  • Roomでデータ格納
  • 格納するデータをMVVMで表示
  • 一部データは後で書き換えられるようにする

手探りで進めた結果ですので参考程度にお願いします。割と雑に書いているけど許して。

つくる

格納するデータ(Entity)定義

こんな感じ。

@Entity(tableName = "prize_history")
data class PrizeHistory(
    @PrimaryKey
    @ColumnInfo(name = "tweet_id")
    val tweetId: String,

    @ColumnInfo(name = "account_id")
    val accountId: String,

    @ColumnInfo(name = "tweet_content")
    val tweetContent: String,

    @ColumnInfo(name = "closing_date")
    val closingDate: MutableLiveData<Date>
)

この時点で書き換えたいデータはMutableLiveDataにしておく。DAO, Databaseはあまり凝ったことはしていないので調べてください。
Date型値はそのままDBに突っ込めないのでConverterも作成しておく。

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): MutableLiveData<Date>? {
        return value?.let { MutableLiveData(Date(it)) }
    }

    @TypeConverter
    fun dateToTimestamp(date: MutableLiveData<Date>?): Long? {
        return date?.value?.time
    }
}

表示するレイアウトのxml

RecylerViewで表示する中の、各行のデータレイアウトの一部を抜粋。

<layout xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
                name="item"
                type="com.rutilicus.twitter_prize_management.PrizeHistory" />
        <variable
                name="viewModel"
                type="com.rutilicus.twitter_prize_management.PrizeHistoryViewModel" />
    </data>
    <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/linearlayout_history_item"
            android:background="@drawable/border">
        <!-- 中略 -->
        <TextView
                android:id="@+id/textview_content_closing"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight=".20"
                android:maxLines="1"
                app:convertDate="@{item.closingDate}"
                android:onClick="@{() -> viewModel.onClickClosingDate(item)}"
        />
        <!-- 後略 -->
    </LinearLayout>
</layout>

あまり凝ったことはしていないと思うが、Date型の値を表示するためにBindingAdapterを使用している。こんな感じのコード。

object BindingAdapters {
    @BindingAdapter("convertDate")
    @JvmStatic
    fun convertDate(view: TextView, date: MutableLiveData<Date>?) {
        view.text = date?.value?.let { SimpleDateFormat("MM/dd", Locale.JAPANESE).format(it) }
}

ViewModel

インターネット上の情報をもういろいろ見て作りました……

class PrizeHistoryViewModel(application: Application) : AndroidViewModel(application) {
    private var itemsRaw = mutableListOf<PrizeHistory>()

    private val _items = MutableLiveData<List<PrizeHistory>>(emptyList())
    val items: LiveData<List<PrizeHistory>> = _items.distinctUntilChanged()

    private val _uiState = MutableStateFlow(PrizeHistoryUiState())
    val uiState: StateFlow<PrizeHistoryUiState> = _uiState

    private var dao: PrizeHistoryDao?

    init {
        if (DatabaseInstance.database == null) {
            DatabaseInstance.createDatabase(getApplication<Application>().applicationContext)
        }
        dao = DatabaseInstance.database?.prizeHistoryDao()
    }

    fun loadPrizeHistory() {
        val loadItems = mutableListOf<PrizeHistory>()

        CoroutineScope(Dispatchers.Main).launch {
            withContext(Dispatchers.Default) {
                // DBからデータ読み出し・設定
                dao?.getAll()?.forEach { loadItems.add(it) }
            }
            itemsRaw = loadItems
            _items.value = ArrayList(itemsRaw)
        }
    }

    fun updateClosingDate(item: PrizeHistory, newDate: Date) {
        itemsRaw.forEach {
            if (it.tweetId == item.tweetId) {
                it.closingDate.value = newDate
                CoroutineScope(Dispatchers.Default).launch {
                    dao?.updateAll(it)
                }
                return@forEach
            }
        }
    }

    fun onClickClosingDate(item: PrizeHistory) {
        viewModelScope.launch {
            _uiState.update { it.copy(dateFixItem = item) }
            return@launch
        }
    }

    fun dateFixDialogShown() {
        _uiState.update { it.copy(dateFixItem = null) }
    }
}

まず、DBアクセスのビジネスロジックをViewModelに入れる都合上、Contextが必要になるのでAndroidViewModelを継承する。initでアクセス用の初期処理を実行。
loadPrizeHistoryは初期表示時のデータ一斉読み込みメソッド。Mainスコープであれこれやるよう書いているけど不要かも。
updateClosingDateは格納するデータのうち後から書き換え対象となるDate型オブジェクトを更新するメソッド。MutableLiveDataの書き換えとDBの書き換えを同じメソッド内で行う。DBアクセスはMainスコープでできないのでDefaultスコープで実行。
onClickClosingDateは更新データ入力用のダイアログを開くトリガを発火させるメソッド。dateFixDialogShownはダイアログ表示後の後処理メソッド。
ダイアログはUI要素なのでViewModel中には記載しないようにする。StateをUI側で監視して、状態に変化があればダイアログを開くようにする。
Stateを格納するdata classはなんてことのないクラス。

data class PrizeHistoryUiState(
    val dateFixItem: PrizeHistory? = null,
)

Fragment

表示するUI側の実装。

class HistoryFragment : Fragment() {

    private lateinit var prizeHistoryListAdapter: PrizeHistoryListAdapter
    private lateinit var viewModel: PrizeHistoryViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        activity?.application?.let {
            viewModel = ViewModelProvider(
                it as MyApplication, PrizeHistoryViewModelFactory(it))[PrizeHistoryViewModel::class.java]
        }
        return FragmentHistoryBinding.inflate(inflater, container, false)
            .apply {
                recyclerviewHistory.run {
                    layoutManager = LinearLayoutManager(context)
                    adapter = PrizeHistoryListAdapter(viewLifecycleOwner, this@HistoryFragment.viewModel).also {
                        prizeHistoryListAdapter = it
                    }
                }
            }.run {
                root
            }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // データ読み出し処理
        viewModel.loadPrizeHistory()

        viewModel.run {
            items.observe(viewLifecycleOwner) {
                prizeHistoryListAdapter.submitList(it)
            }
        }

        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { uiState ->
                    uiState.dateFixItem?.let {
                        Calendar.getInstance().apply {
                            val item = uiState.dateFixItem
                            this.time = item.closingDate.value!!
                            this@HistoryFragment.context?.let { context ->
                                DatePickerDialog(
                                    context,
                                    { _, year, month, dayOfMonth ->
                                        Calendar.getInstance().apply {
                                            this.set(year, month, dayOfMonth)
                                            viewModel.updateClosingDate(item, this.time)
                                        }
                                    },
                                    this.get(Calendar.YEAR),
                                    this.get(Calendar.MONTH),
                                    this.get(Calendar.DAY_OF_MONTH),
                                ).show()
                            }
                        }
                        viewModel.dateFixDialogShown()
                    }
                }
            }
        }
    }
}

onCreateViewの処理については定型のViewModelを使用する場合の処理とは離れていますが、これは別Activityからデータの追加処理を行う都合上こうなっています。Applicationを継承したクラスでViewModelStoreを保持するようにしています。それ以外は凝ったことはしていないはず。
onViewCreatedではデータの初期化処理と、データ更新時のRecyclerViewへの反映のための監視、ダイアログを開くためのトリガ発火時の処理を記載。一応動いているつもりではあるが、あまり自信はない……

できたアプリ

こんな感じのアプリになりました。なんだかクラッシュしているようなのだが、Android Vitalsからレポートが見れない(報告されていない?)ので何が起こっているのか分からない……
play.google.com