画面に文字が飛び交う時限クソチャットアプリ
はじめに
こちらはクソアプリ2 Advent Calendar2018 12月19日の投稿となります。
結論
作りました。
ラスミン共和国
背景
(役に立たない情報なので、この章を読み飛ばしても支障はありません)
昨今、私たちはスマホの普及によって、いつでもどこでもインターネットに繋がる環境を手に入れ、SNSの普及によって誰とでも繋がれる環境を手に入れることができました。
確かに、気になったらすぐ調べたり質問したりできるし、時間が空いたら暇つぶしもできるし、めっちゃ便利! です。
一方で、情報量の爆発と共に「評価」というものの比重が大きくなってきました。
膨大な情報の中から評価の高い情報をユーザーに優先的に表示していく。これは全ての情報を取捨選択する能力のない私たちにとっては重要な機能です。
裏を返せば、私たちは役に立たない情報を得る手段を失いつつある、ということです。
この役に立たない情報。本当に何の役にも立たないのでしょうか。
いえ、そうではなかったはずです。
初めてインターネットに繋いだ時に得られた感動。それは「有益な情報にいくらでもアクセスできるぜぇ!」だけではなくて、「どこかの知らない誰か」と「同じ時間を共有できている」という感動だったのではないでしょうか。
名前も知らない人「お腹すいたのでチキンラーメン作る」
このどうでもいい、私的でリアルタイムな情報。
これを初めて見た時のワクワク感を私たちは今、「評価」という大義のもとに失いつつあります。
どうするか
(役に立たない情報なので、この章を読み飛ばしても支障はありません)
では、どうでもいい言葉を「評価」の枠から度外視して言えて聞ける場を作りましょう。
モデルはニコニコ動画の「時報」という仕組みです。
これは午前0時になるとニコニコ動画を見ていたユーザーが一箇所に強制的に集められて、同じ映像や音を視聴させられる。そういうものです。
みんながバラバラに非同期で行動していたのに、ある一瞬だけわっと集まってワイワイ騒ぐ。これをリアルとネットの世界で実現したらどうなるか、今回はそれをやってみたかったのです。
要するに、バラバラに生活しているみんなをある一定時間だけネット上に集めさせられて適当な言葉を発してもらう。そういうノリです。
時間は「一日の終わり」ということで「23:45~24:00」としました。
リアルタイム性が大事+基本意味がない情報なので「言葉がすぐに消えるチャットアプリ」、ということにします。
誰だか知らない人A「今日は客先いってきたー」
誰だか知らない人B「めんたいこ美味い」
誰だか知らない人C「スーツとか最近着てないなー」
みたいに、誰だか知らない人と一日のどうでもいい私的な情報をやりとりしたり呟いたりしていくアプリを目指しましょう。
世界観
(役に立たない情報なので、この章を読み飛ばしても支障はありません)
このアプリでは、ある時間になると突然みんなが集まってくるわけです。
ぼんやりと街の真ん中にある「広場」のような空間が連想されます。
「広場」でもよいのですが、何となくサービスに対しての帰属意識みたいのがあって欲しいのでコミュニティ的な名前を付けることにしました。ここではとりあえずわかりやすく「国家」とします。
その日の最後の数分だけ使えるので名前は「ラストミニッツ」転じて「ラスミン」にしました。
「国家」という概念に軽く肉付けをします。これまでの経緯から「ユーザーは23時45分には必ず広場に集まる」というルールが存在していることが明らかですので、恐らくは統制社会だろうと想起されます。
たまたま今年読んでいたオーウェルの「1984」の世界観が合いそうですので、そういう感じにしていきます。
普通にサービスを作るときは、もっと世界観を練らないといけませんが、クソアプリなのでとりあえずこれでOKとします。
機能
(役に立たない情報なので、この章を読み飛ばしても支障はありません)
ログイン機能
とりあえずチャットアプリなので、最低限のログイン機能を実装します。最低限のログインとは「ID/パスワード」の組み合わせではなく「名前」だけです。
同じ名前の人がいたら誰が誰だかわからなくなるし、なりすましのリスクもでてきますが、アプリの世界観的に一般ユーザーなんて誰が誰でも別に知ったことではないのでそれでよいものとします。
チャット機能
基本的にどうでもいい会話が行われるはずですので、一瞬出て一瞬で消えるようなUIを実現します。
上から下に文字が流れる普通のチャットでも良いのですが、面白味がないので適当にSVGを使ってガヤガヤできるようにします。
通報機能
変なことを書いてくるユーザーがいるかもしれないので、通報できるようにします。
世界観的に1つ1つ検閲していくのがベストですがリソースがないのでユーザーの公共心に任せることにします。
通報された時点で危険分子の可能性がありますのでユーザーは無条件で追放することにします。
一方で世の中には通報厨という人種もいて無辜の人民を片っ端から通報して楽しむ人もいるので、その対策として通報した場合は一定確率で通報者自身が追放されるようにします。
善意で通報したのに追放されてしまう人が発生してしまう可能性はありますが、善意で通報した人はこのチャットサービスのためを思って通報してくれているということに思考を向けてみたとき、理不尽な追放システムがチャットサービスのためになるという前提に立って考えると、このシステムによって自分自身が一定確率で追放されることは総体としてチャットサービスのためになっていると考えられますので、大局的に考えるとこの理不尽が起こったとしても善意のユーザーの目的は達成しており、従って善意のユーザーはこの理不尽に耐えられることとします。
では作りましょう。
できたもの
タノシソウダネ…
技術
(この章はかろうじて役に立つ可能性があります)
DBもホスティングもGoogle Firebaseを使っています。
権限周りはガバガバですので、あまり弄らないでください。
フロントエンドはVue.jsを使ってますが、以下はクソコードだらけです。
ご寛恕ください。
一番力を入れたのは文字の表示です。
初期化時に
this.timerObj = setInterval(function () { self.countTime() }, 20)
みたいなコードを実行して、20msごとに1ターン分処理するものとして、文字の座標や大きさを変えていきます。
svgの中身はこれだけです。
<svg :viewbox="'0 0 ' + width + ' ' + height" :width="width" :height="height">
<template v-for="text of texts">
<text v-for="partText of text.partialTexts"
:key="partText.id"
:x="partText.x"
:y="partText.y"
:opacity="partText.opacity"
:fill="partText.color"
:font-size="partText.fontSize"
:transform="'rotate(' + partText.rotateZ + ')' + ' translate(' + partText.translateX + ',' + partText.translateY + ')'"
style="cursor: pointer;"
v-on:mouseover="stopTextMove(text)"
v-on:click="showMenuOnText(text)">
{{ partText.texts }}
</text>
</template>
</svg>
ただひたすら「texts」の中身を変えていけば良いわけですね。
飛び跳ねる文字
簡単です。y軸の初期速度をマイナス方向(画面の上方向)に向けておいて、速度に0.98を雑に足していくだけです。(定数0.98は気分で決めた)
addMoveAsGravitation: function (text) {
let hasValidPartialText = false
for (let j = 0; j < text.partialTexts.length; j++) {
var partialTexts = text.partialTexts[j]
partialTexts.x = partialTexts.x + partialTexts.xSpeed
partialTexts.y = partialTexts.y + partialTexts.ySpeed
partialTexts.ySpeed += 0.98
if (partialTexts.y <= this.height) {
hasValidPartialText = true
}
}
return hasValidPartialText
},
サイズがドンドン小さくなる文字
文字列を1文字ずつに分解して、1文字1文字ごとに時間差をつけてOpacityを変え、後は徐々に文字を小さくするだけです。
addMoveAsStamp: function (text) {
let hasWaitText = false
let hasScaledText = false
for (let j = 0; j < text.partialTexts.length; j++) {
var partialTexts = text.partialTexts[j]
if (partialTexts.waitFor > 0) {
partialTexts.waitFor -= 1
if (partialTexts.waitFor <= 0) {
partialTexts.opacity = 1
partialTexts.fontSize = 102
} else {
hasWaitText = true
}
}
if (partialTexts.fontSize >= 24) {
partialTexts.fontSize -= 2
hasScaledText = true
}
}
return hasScaledText || hasWaitText
},
同じ場所に1文字1文字表示されていく文字
文字列を1文字ずつに分解して、1文字1文字ごとに時間差をつけてopacityを変え、表示後はopacityを0に近づけていくだけです
addMoveAsImpact: function (text) {
let hasWaitText = false
for (let j = 0; j < text.partialTexts.length; j++) {
var partialTexts = text.partialTexts[j]
if (partialTexts.waitFor > 0) {
partialTexts.waitFor -= 1
if (partialTexts.waitFor <= 0) {
partialTexts.opacity = 1
partialTexts.fontSize = 48
} else {
hasWaitText = true
}
} else {
partialTexts.opacity =
Math.max(partialTexts.opacity - 0.05, 0)
}
}
return hasWaitText
},
グルグル回る文字
初期配置時に文字列の最初の1文字だけを分離して、7個分複製。ループで少しずつずらしながら三角関数を使って同一円周上に配置します。
x, y, は円の中心座標。colorsは色の配列です。
var baseR = 10
for (let i = 0; i < 8; i++) {
var rad = 360 * i / 8 * (Math.PI / 180)
partialTexts.push({
id: id + String(i),
baseX: x,
baseY: y,
baseR: baseR,
color: colors[i],
x: Math.cos(rad) * baseR + x,
y: Math.sin(rad) * baseR + y,
texts: str[0],
opacity: 1.0,
fontSize: 24,
translateX: 0,
translateY: 0,
rotateZ: 0,
rad: i
})
}
あとは、三角関数でグルグル回します。(定数49は気分です)
addMoveAsRound: function (text) {
for (let j = 0; j < text.partialTexts.length; j++) {
var partialTexts = text.partialTexts[j]
if (partialTexts.rad == null) {
continue
}
partialTexts.rad = (partialTexts.rad + 1) % 49
var rad = 360 * partialTexts.rad / 49 * (Math.PI / 180)
partialTexts.x = partialTexts.baseX + Math.sin(rad) * partialTexts.baseR
partialTexts.y = partialTexts.baseY + Math.cos(rad) * partialTexts.baseR
}
text.moveCount -= 1
return text.moveCount >= 0
},
震える文字
transformのtranslate(X, Y)とrotateZを定期的に変えるだけです。
for (let j = 0; j < text.partialTexts.length; j++) {
var partialTexts = text.partialTexts[j]
partialTexts.shakeTime = (partialTexts.shakeTime + 1) % 4
switch (partialTexts.shakeTime) {
case 0:
partialTexts.translateX = 0
partialTexts.translateY = 0
partialTexts.rotateZ = 0
break
case 1:
partialTexts.translateX = 2
partialTexts.translateY = 2
partialTexts.rotateZ = 1
break
case 2:
partialTexts.translateX = 0
partialTexts.translateY = 2
partialTexts.rotateZ = 0
break
case 3:
partialTexts.translateX = 2
partialTexts.translateY = 0
partialTexts.rotateZ = -1
break
}
}
text.moveCount -= 1
return text.moveCount >= 0
バラバラの状態から最後に結合する文字
まず文字ごとに最終的なX,Y座標だけ先に決めておきます。
初期状態で1文字ずつバラバラに配置して、100ターン経過したときに、それぞれ最終座標に落ち着いているようにX,Yの速度を決めるだけです。
初期設定
const x = (Math.random() * (this.width - (str.length * 24)))
const y = (Math.random() * this.height)
let partialTexts = []
for (let i = 0; i < str.length; i++) {
const currentX = (Math.random() * this.width)
const currentY = (Math.random() * this.height)
const currentRotate = Math.random() * 360
const moveX = (x + (24) * i - currentX) / 100
const moveY = (y - currentY) / 100
const moveRotate = (0 - currentRotate) / 100
partialTexts.push({
id: id + String(i),
moveX: moveX,
moveY: moveY,
moveRotate: moveRotate,
x: currentX,
y: currentY,
color: color,
texts: str[i],
opacity: 1.0,
fontSize: 24,
moveCount: 101,
translateX: 0,
translateY: 0,
rotateZ: 0
})
}
移動
for (let j = 0; j < text.partialTexts.length; j++) {
var partialTexts = text.partialTexts[j]
partialTexts.moveCount -= 1
if (partialTexts.moveCount >= 1) {
partialTexts.x += partialTexts.moveX
partialTexts.y += partialTexts.moveY
}
// partialTexts.rotateZ += partialTexts.moveRotate
}
text.moveCount -= 1
return text.moveCount >= 0
終わりに
クソアプリに栄光あれ!