壁尻になれるWebアプリ作った

[クソアプリ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/kuso-app) 16日目の記事です。


## 壁尻とは
>いわゆる「埋め込み系」と称されるジャンルの中でも、
上半身と下半身が分断されているのが特徴。
自分の下半身の状態を確認できないもどかしさや、
尻だけが出ている状況のシュールさ、
顔の見えない下半身を犯すフェティッシュな背徳感が肝。
>
>[壁尻 (かべしり)とは【ピクシブ百科事典】](https://dic.pixiv.net/a/%E5%A3%81%E5%B0%BB)より

つまり壁から下半身が出ているシチュエーションのことです。
特に成人向け作品では、尻の近くに顔写真が貼ってあることが多いです。フェティッシュですね。

今回は壁尻になりたい皆さんの願いを叶えるWebアプリを作りました。

## 完成品
こちらです。
https://kabeshiri-maker.utabami.com/

![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409314/fe7805dd-8bb4-6a85-e89a-c053650e2c5b.png)
スタイルを選んで、SNS連携をします。

![キャプチャ1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409314/83ba993b-324f-376c-1d73-5f3d932a43b2.png)
あとは3秒くらい待つだけで憧れの壁尻になれます。
（Facebookでやる猛者はいるのだろうか……）

壁尻完成画面でシェアすれば、SNS上でもOGPで画像が表示されます。みんなに見てもらいましょう。

## 使用技術
- Nuxt.js
- Vuetify
- Firebase Authentication
- Firebase Storage
- Firebase Cloud Firestore
- Netlify

めけぽんビンゴの改装でNetlifyのPrerenderingを使う予定なので、それに合わせた技術になっています。そうです、今回の主な目的はPrerenderingのお試しです。
プロフィール画像と名前はFirebase Authenticationで取得できるので、そのまま使います。

### 実装

#### 壁尻画像を生成する
```javascript:
async makeImage(style) {
  //キャンバスの用意
  const canvas = document.createElement('canvas')
  var ctx = canvas.getContext('2d')
  canvas.width = 600
  canvas.height = 300

  //背景画像を描画
  const baseImage = await loadImage(
    require('~/assets/kabe_' + style + '.png')
  )
  ctx.drawImage(baseImage, 0, 0)

  //アイコンを描画
  const profileImage = await loadImage(auth.user.photoURL)
  ctx.drawImage(profileImage, 320, 40, 96, 96)

  //テキストを描画
  ctx.font = "18px 'M PLUS Rounded 1c'"
  ctx.fillText(auth.user.displayName, 320, 160)

  return canvas.toDataURL()
}

//画像読み込み
function loadImage(url) {
  return new Promise(resolve => {
    const img = new Image()
    img.onload = () => {
      resolve(img)
    }
    img.onerror = e => {
      console.log(e)
      resolve()
    }
    img.setAttribute('crossOrigin', 'Anonymous')
    img.crossOrigin = 'Anonymous'
    img.src = url
  })
}
```
今回は複雑な画像の加工はしないので、HTML5 Canvasでモリモリッと作っています。
余談ですが、もう少し複雑なことをするときは[Fabric.js](http://fabricjs.com/)というライブラリを使っています。

#### 完成した壁尻を表示するページ
```vuejs
<template>
  <v-layout column justify-center align-center>
    <v-flex class="headline my-6" xs12 sm8 md6>{{name}}の壁尻</v-flex>
    <v-flex xs12 sm8 md6>
      <v-card class="mb-5">
        <v-card-text>
          <v-img :src="image"/>
        </v-card-text>
        <v-card-text>
          <v-layout column justify-center>
            <v-flex xs12>
              <v-btn
                class="mb-3"
                color="#55acee"
                style="width:100%"
                x-large
                @click="share('twitter')"
              >Twitterに投稿</v-btn>
            </v-flex>
            <v-flex xs12>
              <v-btn
                class="mb-3"
                color="#385185"
                style="width:100%"
                x-large
                @click="share('facebook')"
              >Facebookにシェア</v-btn>
            </v-flex>
          </v-layout>
        </v-card-text>
      </v-card>
      <makeKabeshiri/>
    </v-flex>
  </v-layout>
</template>

<script>
import { store } from '~/plugins/app'
import makeKabeshiri from '~/components/makeKabeshiri'

export default {
  components: {
    makeKabeshiri
  },
  data: () => ({
    name: '',
    image: ''
  }),
  async asyncData({ params }) {
    //データセットしていく
    const user = await store
      .collection('users')
      .doc(params.userId)
      .get()
    return {
      name: user.data().displayName,
      image: user.data().image
    }
  },
  head() {
    return {
      title: this.name + 'の壁尻',
      meta: [
        {
          hid: 'og:image',
          property: 'og:image',
          content: this.image
        },
        {
          hid: 'twitter:image',
          name: 'twitter:image',
          content: this.image
        }
      ]
    }
  },
  methods: {
    share(target) {
      let shareUrl = ''
      if (target === 'twitter') {
        const baseUrl = 'https://twitter.com/intent/tweet?'
        const text = ['text', '壁尻になりました！']
        const hashtags = ['hashtags', 'みんなで壁尻メーカー']
        const url = ['url', location.href]
        const query = new URLSearchParams([text, hashtags, url]).toString()
        shareUrl = `${baseUrl}${query}`
      } else if (target === 'facebook') {
        const baseUrl = 'https://www.facebook.com/sharer/sharer.php?'
        const url = ['u', location.href]
        const query = new URLSearchParams([url]).toString()
        shareUrl = `${baseUrl}${query}`
      }
      window.open(
        shareUrl,
        'share',
        'width=600, height=400, personalbar=0, toolbar=0, scrollbars=1, sizable=1'
      )
    }
  }
}
</script>


```
NuxtのasyncDataってSPAモードでも動くんですね。偏見でSSRでしか動かないと思っていました。

#### NetlifyのPre-RenderingをON！
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409314/971c6b4a-2940-8d00-5491-db53767a7268.png)
NetlifyのSettings > Build & deploy > Prerenderingのチェックを付けます。
これだけでjsで設定したOGP画像が表示されます！すごい！！！しかも結構早い！！！

## ハマった？ところ
#### プロフィール画像はCORSによる制限が緩い
今までの経験で「外部画像をCanvasに読み込む場合、CORSでひっかかってうまく読み込めないことが多い」ということが身に染みていたので、当初はFirebase Functionsをプロキシ代わりにするつもりで実装してたのですが、後になってプロフィール画像は"Access-Control-Allow-Origin: *"になっていることに気づきました。
プロフィール画像はどんなサイトでも使えるようにしてあるのですね。

#### パンツ密着させすぎた
このサービスで使っているお尻は自前で描きました。冷静に見ると、女のほうのパンツを密着させすぎた感がありますね。もうちょっと密着度低めのリアルよりのほうがよかったかも。

## まとめ
ということでNetlifyのPrerenderingのお試しとしてクソアプリを作ってみました。
せっかくなら完全成人向けにして、アイコンでなく自分で描いたイラストが使えたり、あらかじめ用意されたセリフや液体や落書きをレイヤーで重ねて実用性を上げてみたいところです。
需要ありそうならやるかもしれませんので、その時はよろしくお願いします。