Help us understand the problem. What is going on with this article?

Vue.jsでFF7風のポートフォリオを作った

作ったもの

ff7.gif

経緯

ff7のリメイクがもうすぐ発売されますね!!!
早くプレイしたいです。

待ち遠しさを紛らわすために、遊び心でポートフォリオとして作成してみました。
マテリアをCSSで記載するなど、しなくてよい努力を詰め込んでますw

使ったもの

  • Vue.js
  • Nuxt.js
  • Vuetify
  • Typescript
  • Scss

フロントの勉強かつ、Typescriptの練習を兼ねて上記のような構成にしています。

Top画面の説明

ff7_top.png

メニュー画面にマウスカーソルを合わせると、あのお馴染みのカーソルを表示するようにしています。クリックするとページを遷移するように作成していますが、現在は「マテリア」ページのみ作成しております。

背景色

ff7の背景色は、下記のscssで作成しています。ff7-cardクラスを指定すれば、あの青い背景色になります。

$text-color: #eff1ff;
$background-color: #04009d;
$background-color-dark: #06004d;

.ff7-card {
    border: solid 1px #424542;
    box-shadow: 1px 1px #e7dfe7, -1px -1px #e7dfe7, 1px -1px #e7dfe7,
      -1px 1px #e7dfe7, 0 -2px #9c9a9c, -2px 0 #7b757b, 0 2px #424542;
    padding: 5px 10px;

    background: $background-color;
    background: -moz-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: -webkit-gradient(
      linear,
      left top,
      left bottom,
      color-stop(0%, $background-color),
      color-stop(100%, $background-color-dark)
    );
    background: -webkit-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: -o-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: -ms-linear-gradient(top, $background-color 0%, $background-color-dark 100%);
    background: linear-gradient(to bottom, $background-color 0%, $background-color-dark 100%);
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$background-color', endColorstr='$background-color-dark',GradientType=0 );

    -webkit-border-radius: 7px;
    -moz-border-radius: 7px;
    border-radius: 7px;
    * {
        color: $text-color;
        text-shadow: 2px 2px #212421, 1px 1px #212021;
        font-family: Verdana, sans-serif;
        font-weight: normal;    
    }
}

メニューの部分

メニューの部分を説明していきます。

<template>
  <section>
    <v-container class="top-menu-list ff7-card">
      <v-row
        v-for="(item, index) in topMenuItems"
        :key="index"
        no-gutters
        @mouseleave="menuMouseleave(index)"
        @mouseover="menuMouseover(index)"
      >
        <v-col cols="12">
          <cursor-parts v-if="menuChoice == index" />
          <nuxt-link
            v-if="item.display == true"
            :to="item.path"
            class="top-menu-item"
          >
            {{ item.name }}
          </nuxt-link>
        </v-col>
      </v-row>
    </v-container>
  </section>
</template>

topMenuItemsの内容を表示するようにしています。また、マウスカーソルの操作は@mouseleave @mouseoverで対応しています。クリックした場合nuxt-linkで指定先に遷移します。

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'
import CursorParts from '~/components/parts/CursorParts.vue'

@Component({
  components: { CursorParts }
})
export default class TopMenu extends PortfolioVueEx {
  menuChoice: number = -1
  topMenuItems: {
    name: string
    path: string
    display: boolean
  }[] = [
    {
      name: 'アイテム',
      path: '/',
      display: true
    },
    {
      name: 'まほう',
      path: '/',
      display: true
    },
    {
      name: 'マテリア',
      path: 'materia',
      display: true
    },
    {
      name: 'そうび',
      path: '/',
      display: true
    },
    {
      name: 'ステータス',
      path: '/',
      display: true
    },
    {
      name: 'たいけい',
      path: '/',
      display: true
    },
    {
      name: 'リミット',
      path: '/',
      display: true
    },
    {
      name: 'コンフィグ',
      path: '/',
      display: true
    },
    {
      name: 'PHS',
      path: '/',
      display: true
    },
    {
      name: 'セーブ',
      path: '/',
      display: true
    }
  ]

  menuMouseover(i: number) {
    this.menuChoice = i
  }
  menuMouseleave() {
    this.menuChoice = -1
  }
}
</script>

<style scoped lang="scss">
.top-menu-item {
  margin: 10px;
}

.top-menu-list {
  text-align: left;
  width: 143px;
}

a:link {
  text-decoration: none;
  color: white;
}

a:visited {
  text-decoration: none;
  color: white;
}
</style>

メニューの内容は、topMenuItemsにリスト型で保持しています。

キャラクター

TopTeam.vue
<template>
  <section>
    <v-container class="top-team-list ff7-card">
      <v-row
        v-for="(item, index) in $store.state.players"
        :key="index"
        align="center"
      >
        <v-col cols="4">
          <v-row justify="center">
            <img :src="item.image" width="75" />
          </v-row>
        </v-col>
        <v-col cols="3">
          {{ item.name }}{{ $store.state.level }}
          <v-row>
            <span class="status-item">LV</span>
            <span class="level-margin status-content">{{ item.level }}</span>
          </v-row>
          <v-row>
            <span class="status-item">HP</span>
            <span class="status-content">
              <span>
                {{ item.hp }}/{{ item.maxHp }}
                <div class="progress-linear">
                  <progress-hp-parts
                    :parent-max.sync="item.maxHp"
                    :parent-value.sync="item.hp"
                  />
                </div>
              </span>
            </span>
          </v-row>
          <v-row>
            <span class="status-item">MP</span>
            <span class="status-content">
              <span class="mp-margin">{{ item.mp }}/</span>
              <span class="mp-margin">{{ item.maxMp }}</span>
              <div class="progress-linear">
                <progress-mp-parts
                  :parent-max.sync="item.maxMp"
                  :parent-value.sync="item.mp"
                />
              </div>
            </span>
          </v-row>
        </v-col>
        <v-col cols="3">
          <v-row>
            <span class="next-level">つぎのレベルまであと</span>
            <progress-parts
              :parent-max.sync="item.maxExp"
              :parent-value.sync="item.exp"
              class="next-level-margin"
            />
          </v-row>
          <br />
          <v-row>
            <span class="limit">リミットレベル {{ item.limitLevel }}</span>
            <progress-parts
              :parent-max.sync="item.maxLimit"
              :parent-value.sync="item.limit"
              class="limit-margin"
            />
          </v-row>
        </v-col>
      </v-row>
    </v-container>
  </section>
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'
import ProgressParts from '~/components/parts/ProgressParts.vue'
import ProgressHpParts from '~/components/parts/ProgressHpParts.vue'
import ProgressMpParts from '~/components/parts/ProgressMpParts.vue'

@Component({
  components: {
    ProgressParts,
    ProgressHpParts,
    ProgressMpParts
  }
})
export default class TopTeam extends PortfolioVueEx {
  menuChoice: number = -1
}
</script>

<style scoped lang="scss">
.top-team-list {
  padding: 0px 0px 0px 30px;
  width: 457px;
  height: 360px;
}

.progress-linear {
  margin: -15px 0px 0px 0px;
}

.status-item {
  color: #00ddd6;
  margin-right: 10px;
  font-size: 100%;
}
.status-content {
  font-size: 80%;
}
.level-margin {
  margin-top: 3px;
  margin-left: 18px;
}
.mp-margin {
  margin-left: 6px;
}

.next-level {
  font-size: 60%;
}
.next-level-margin {
  margin-left: 15px;
  margin-right: 0px;
}

.limit {
  font-size: 60%;
}
.limit-margin {
  margin-left: 15px;
  margin-right: 0px;
}
</style>

'store'でキャラクターの情報(LV/HP/MPなど)を保持しており、それらを呼び出すようにしています。

ゲージは別途コンポーネントを作成しております。
- ProgressHpPartsは、HPのゲージ
- ProgressMpPartsは、MPのゲージ
- ProgressPartsは、経験値とリミットのゲージ

マテリア画面の説明

ff7_materia.png

マテリアにマウスカーソルを合わせると、カーソルとマテリアの説明が表示されます。マテリア毎に、自分のスキルを乗せるようにしてみました。
詳細説明とマテリアリストの部分は時間が足りなかったので、今後追加していきます。

ちなみに、マテリアを外すと下記のようになります。マテリア穴までCSSで作成しています。見えないところにもこだわる!
ff7_no_materia.png

マテリア

マテリアはコンポーネントとして作成しています。画像でしたらはるかに楽なのですが、CSSで無駄に開発しました。

MateriaParts.vue
<template>
  <div class="wrapper">
    <div class="materia" :style="{ background: color }" />
    <div class="downlight1" />
    <div class="highlight2" />
    <div class="highlight3" />
  </div>
</template>

<script lang="ts">
import { Component, Prop } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'

@Component({})
export default class MateriaParts extends PortfolioVueEx {
  @Prop()
  color: string
}
</script>

<style scoped lang="scss">
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  background: linear-gradient(135deg, #f7f9fc 0%, #e1e7f0 100%);
  display: flex;
  justify-content: center;
  align-items: center;
}

.wrapper {
  width: 20px;
  height: 20px;
  position: relative;
}
.materia {
  width: 20px;
  height: 20px;
  z-index: 0;
  border-radius: 50%;
  background: rgb(43, 100, 21, 1);
}
.downlight1 {
  position: absolute;
  top: 10%;
  left: 15%;
  z-index: 1;
  width: 12px;
  height: 12px;
  background: rgba(0, 0, 0, 1);
  border-radius: 50%;
  filter: blur(2px);
  opacity: 0.6;
}
.highlight2 {
  position: absolute;
  top: 20%;
  left: 25%;
  z-index: 1;
  width: 3px;
  height: 1px;
  border-radius: 50%;
  border-top: 0.5px solid #fff;
  transform: rotate(-70deg) scaleX(0.9) scaleY(1.5) skewY(18deg);
  filter: blur(0.6px);
}
.highlight3 {
  position: absolute;
  top: 0%;
  left: 0%;
  width: 18px;
  height: 18px;
  background-color: transparent;
  box-shadow: inset -3px -6px 0 -3px rgba(255, 255, 255, 1);
  border-radius: 50%;
  filter: blur(1px);
  opacity: 0.15;
}
</style>

球型に、ハイライトを2種類、ダウンライトを1種類を乗せてマテリアを表現しています。マテリアの色は、Propで指定できるようにしています。

魔法マテリア

各マテリアは、MateriaParts.vueに色を指定しています。魔法マテリアは下記になります。

MagicMateria.vue
<template>
  <materia-parts :color="'rgb(43, 100, 21, 1)'" />
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PortfolioVueEx from '~/logic/vue/PortfolioVueEx'
import MateriaParts from '~/components/parts/MateriaParts.vue'

@Component({
  components: { MateriaParts }
})
export default class MagicMateria extends PortfolioVueEx {}
</script>

マテリア画面

突貫で作ったので、改良の余地がありまくりです。。。

materia.vue
<template>
  <v-container>
    <v-row align="center" justify="center">
      <div class="materia-box">
        <div class="character-box ff7-card">
          <v-row align="center">
            <v-col cols="2">
              <v-row justify="center">
                <img :src="$store.state.players[0].image" width="75" />
              </v-row>
            </v-col>
            <v-col cols="3">
              {{ $store.state.players[0].name }}{{ $store.state.level }}
              <v-row>
                <span class="status-item">LV</span>
                <span class="level-margin status-content">
                  {{ $store.state.players[0].level }}
                </span>
              </v-row>
              <v-row>
                <span class="status-item">HP</span>
                <span class="status-content">
                  <span>
                    {{ $store.state.players[0].hp }}/{{
                      $store.state.players[0].maxHp
                    }}
                    <div class="progress-linear">
                      <progress-hp-parts
                        :parent-max.sync="$store.state.players[0].maxHp"
                        :parent-value.sync="$store.state.players[0].hp"
                      />
                    </div>
                  </span>
                </span>
              </v-row>
              <v-row>
                <span class="status-item">MP</span>
                <span class="status-content">
                  <span class="mp-margin">
                    {{ $store.state.players[0].mp }}/
                  </span>
                  <span class="mp-margin">
                    {{ $store.state.players[0].maxMp }}
                  </span>
                  <div class="progress-linear">
                    <progress-mp-parts
                      :parent-max.sync="$store.state.players[0].maxMp"
                      :parent-value.sync="$store.state.players[0].mp"
                    />
                  </div>
                </span>
              </v-row>
            </v-col>
            <v-col cols="7">
              <v-row>
                <span class="attack-margin">
                  <span class="status-item">武器:</span>
                  <span>ノートPC</span>
                </span>
              </v-row>
              <v-row class="equipment-margin">
                <div
                  v-for="(item, index) in attackMaterias"
                  :key="index"
                  no-gutters
                  class="equipment"
                  @mouseleave="menuMouseleave(index)"
                  @mouseover="menuMouseover(item, index)"
                >
                  <cursor-parts v-if="menuChoice == index" />
                  <command-materia v-if="item.type == 1" class="content" />
                  <independent-materia
                    v-else-if="item.type == 2"
                    class="content"
                  />
                  <magic-materia v-else-if="item.type == 3" class="content" />
                  <summon-materia v-else-if="item.type == 4" class="content" />
                  <support-materia v-else-if="item.type == 5" class="content" />
                  <div v-if="item.type !== 0" class="highlight4" />
                </div>
              </v-row>
              <v-row>
                <span class="defence-margin">
                  <span class="status-item">防具:</span>
                  <span>お供のコーヒー</span>
                </span>
              </v-row>
              <v-row class="equipment-margin">
                <div
                  v-for="(item, index) in defenceMaterias"
                  :key="index"
                  no-gutters
                  class="equipment"
                  @mouseleave="menuMouseleave(index + 8)"
                  @mouseover="menuMouseover(item, index + 8)"
                >
                  <cursor-parts v-if="menuChoice == index + 8" />
                  <command-materia v-if="item.type == 1" class="content" />
                  <independent-materia
                    v-else-if="item.type == 2"
                    class="content"
                  />
                  <magic-materia v-else-if="item.type == 3" class="content" />
                  <summon-materia v-else-if="item.type == 4" class="content" />
                  <support-materia v-else-if="item.type == 5" class="content" />
                  <div v-if="item.type !== 0" class="highlight4" />
                </div>
              </v-row>
            </v-col>
          </v-row>
        </div>
        <div class="message-box ff7-card">
          <v-row v-if="selectedMateria !== ''">
            <command-materia class="content" v-if="selectedMateria.type == 1" />
            <independent-materia
              class="content"
              v-else-if="selectedMateria.type == 2"
            />
            <magic-materia
              class="content"
              v-else-if="selectedMateria.type == 3"
            />
            <summon-materia
              class="content"
              v-else-if="selectedMateria.type == 4"
            />
            <support-materia
              class="content"
              v-else-if="selectedMateria.type == 5"
            />
            {{ selectedMateria.name }}
          </v-row>
        </div>
        <div class="materias-box ff7-card"></div>
        <div class="status-box ff7-card">
          {{ description }}
        </div>
        <div class="page-box ff7-card">
          マテリア
        </div>
      </div>
    </v-row>
  </v-container>
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator'
import PageBase from '~/logic/vue/PageBase'
import CommandMateria from '~/components/templates/CommandMateria.vue'
import IndependentMateria from '~/components/templates/IndependentMateria.vue'
import MagicMateria from '~/components/templates/MagicMateria.vue'
import SummonMateria from '~/components/templates/SummonMateria.vue'
import SupportMateria from '~/components/templates/SupportMateria.vue'
import ProgressParts from '~/components/parts/ProgressParts.vue'
import ProgressHpParts from '~/components/parts/ProgressHpParts.vue'
import ProgressMpParts from '~/components/parts/ProgressMpParts.vue'
import CursorParts from '~/components/parts/CursorParts.vue'

@Component({
  components: {
    CommandMateria,
    IndependentMateria,
    MagicMateria,
    SummonMateria,
    SupportMateria,
    ProgressParts,
    ProgressHpParts,
    ProgressMpParts,
    CursorParts
  }
})
export default class Materia extends PageBase {
  selectedMateria: any = ''
  description!: string
  details!: string
  menuChoice: number = -1
  attackMaterias: {
    name: string
    description: string
    details: string
    type: number
  }[] = [
    {
      name: 'Golang',
      description: 'Golang を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Solidity(Ethereum)',
      description: 'Solidity(Ethereum) を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Python',
      description: 'Python を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'C言語',
      description: 'C言語を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Java',
      description: 'Java を使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Mysql',
      description: 'Mysqlを使えます。',
      details: '',
      type: 3
    },
    {
      name: 'Typescript',
      description: 'Typescriptを使えます。',
      details: '',
      type: 1
    },
    {
      name: 'SCSS',
      description: 'SCSS を使えます。',
      details: '',
      type: 1
    }
  ]
  defenceMaterias: {
    name: string
    description: string
    details: string
    type: number
  }[] = [
    {
      name: 'AWS',
      description: 'Amazon Web Services を使えます。',
      details: '',
      type: 4
    },
    {
      name: 'Kubernetes',
      description: 'Kubernetesを使えます。',
      details: '',
      type: 4
    },
    {
      name: 'SpringBoot',
      description: 'SpringBoot のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'Vue.js',
      description: 'Vue.js のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'Nuxt.js',
      description: 'Nuxt.js のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'Vuetify',
      description: 'Vuetify のフレームワークを使えます。',
      details: '',
      type: 2
    },
    {
      name: 'GitHub',
      description: 'GitHubを使えます。',
      details: '',
      type: 5
    },
    {
      name: 'Unity',
      description: 'Unity を使えます。',
      details: '',
      type: 5
    }
  ]

  menuMouseover(item: any, i: number) {
    this.menuChoice = i
    this.selectedMateria = item
    this.description = item.description
  }
  menuMouseleave() {
    this.menuChoice = -1
  }
}
</script>

<style scoped lang="scss">
.materia-box {
  position: relative;
}

.character-box {
  position: relative;
  width: 600px;
  height: 150px;
  margin: 6px 0 6px 0;
}
.message-box {
  position: relative;
  z-index: 2;
  width: 386px;
  height: 300px;
  margin: 0px 0px 0px 0px;
  padding: 60px 0px 0px 20px;
  float: left;
  .content {
    margin: 0px 6px 0px 0px;
  }
}
.materias-box {
  position: relative;
  z-index: 1;
  left: 380px;
  width: 220px;
  height: 300px;
  padding: 60px 0px 0px 15px;
}
.status-box {
  position: relative;
  z-index: 3;
  width: 600px;
  height: 50px;
  top: -300px;
  display: flex;
  align-items: center;
}
.page-box {
  position: relative;
  z-index: 2;
  width: 150px;
  height: 40px;
  top: -506px;
  left: 450px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.progress-linear {
  margin: -15px 0px 0px 0px;
}

.status-item {
  color: #00ddd6;
  margin-right: 10px;
  font-size: 100%;
}
.status-content {
  font-size: 80%;
}
.level-margin {
  margin-top: 3px;
  margin-left: 18px;
}
.mp-margin {
  margin-left: 6px;
}
.attack-margin {
  margin: 6px 0px 6px 0;
}
.defence-margin {
  margin: 6px 0px 6px 0;
}
.equipment {
  margin: 0px 3px 0px 3px;
  width: 24px;
  height: 24px;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(
    closest-side at 49% 49%,
    rgb(150, 150, 150) 0%,
    rgb(40, 40, 40) 25%,
    rgb(40, 40, 40) 70%,
    rgb(100, 100, 100) 92%
  );
  background-color: transparent;
  box-shadow: inset 6px 6px 2px -6px rgba(200, 200, 200, 1);
  .content {
    position: relative;
    top: 2.2px;
    left: 1px;
  }
}
.equipment-margin {
  margin-left: 30px;
}
.highlight4 {
  position: relative;
  top: -40%;
  left: 40%;
  width: 4px;
  height: 4px;
  z-index: 2;
  border-radius: 50%;
  background: rgba(150, 150, 150, 1);
  filter: blur(1px);
}
</style>

attackMateriasdefenceMateriasにマテリアを指定しています。typeでマテリアの種類を指定します。ポートフォリオとして、プログラミングスキルを下記のように当てはめています。

  • 0: マテリアなし
  • 1: コマンドマテリア→ フロントスキル
  • 2: 独立マテリア → フレームワーク
  • 3: 魔法マテリア → バックエンドスキル
  • 4: 召喚マテリア → インフラ周りのスキル
  • 5: サポートマテリア→ ツール類

おわりに

思い付きで始めたものの、面白くできました。途中感は否めないですが、ひとまず形にはなりました。
何かアイディアある方はコメント頂けると嬉しいです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした