23
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

クソアプリAdvent Calendar 2019

Day 16

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

Last updated at Posted at 2019-12-15

クソアプリ Advent Calendar 2019 16日目の記事です。

壁尻とは

いわゆる「埋め込み系」と称されるジャンルの中でも、
上半身と下半身が分断されているのが特徴。
自分の下半身の状態を確認できないもどかしさや、
尻だけが出ている状況のシュールさ、
顔の見えない下半身を犯すフェティッシュな背徳感が肝。

壁尻 (かべしり)とは【ピクシブ百科事典】より

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

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

完成品

こちらです。
https://kabeshiri-maker.utabami.com/

キャプチャ2.PNG
スタイルを選んで、SNS連携をします。

キャプチャ1.PNG
あとは3秒くらい待つだけで憧れの壁尻になれます。
(Facebookでやる猛者はいるのだろうか……)

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

使用技術

  • Nuxt.js
  • Vuetify
  • Firebase Authentication
  • Firebase Storage
  • Firebase Cloud Firestore
  • Netlify

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

実装

壁尻画像を生成する

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というライブラリを使っています。

完成した壁尻を表示するページ

<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
NetlifyのSettings > Build & deploy > Prerenderingのチェックを付けます。
これだけでjsで設定したOGP画像が表示されます!すごい!!!しかも結構早い!!!

ハマった?ところ

プロフィール画像はCORSによる制限が緩い

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

パンツ密着させすぎた

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

まとめ

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

23
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?