目次
はじめに
前回の記事ではポーカーアプリを作る準備にあたって、Vue3+CompositionAPIの環境構築
を行いました。
今回は実際に作成したソースコードについて説明していきます。
前回の記事はこちら
作成したポーカーゲームはこちら
作成したファイルについて
プロジェクトディレクトリの"/src/"配下のパスがアプリのソースコードになっており、
今回は手を入れた以下のファイルに絞って説明していきます。
App.vue store - index.ts assets - json/cardList.json components - PorkerGame.vue - ResultGame.vue - StartGame.vue - PorkerGame - HandCard.vue - HandResult.vue
ソースコードの説明
App.vue
ルートパスアクセスで読み込まれるファイル。
Vuexで定義したstateのscreenTypeの値でスタート画面、ゲーム画面、結果画面のいずれかの画面を表示している。
<template> <div class="main"> <div class="main-screen"> <start-game v-if="store.getters.getScreenType === 0" /> <porker-game v-if="store.getters.getScreenType === 1" /> <result-game v-if="store.getters.getScreenType === 2" /> </div> </div> </template>
<script lang="ts"> import { useStore } from "vuex"; import { key } from "@/store/"; import { defineComponent } from "vue"; import StartGame from "@/components/StartGame.vue"; import PorkerGame from "@/components/PorkerGame.vue"; import ResultGame from "@/components/ResultGame.vue";
export default defineComponent({ components: { StartGame, PorkerGame, ResultGame, }, setup() { const store = useStore(key);
return { store, }; }, }); </script>
<style lang="scss" scoped> .main { &-screen { width: 800px; height: 400px; padding: 40px; margin: 40px auto; border: 3px solid lightgray; border-radius: 6px; } } </style>
/store/index.ts
Vuexのstoreファイル。
異なるcomponentで値を共有するために使用しており、
本アプリでは以下の4つのstateを定義しています。
1.screenType
表示画面を管理
2.selectCardIndexList
交換対象のカードを管理
3.handChangeDone
ハンドの交換判定を管理
4.resultArray
ゲーム結果を格納したリストを管理
import { InjectionKey } from "vue";
import { createStore, Store } from "vuex";
export interface State {
screenType: number; // 表示画面 0:スタート画面 1:ゲーム画面 2:結果画面
selectCardIndexList: number[]; // 交換対象のカードリスト
handChangeDone: boolean; // 交換済み判定フラグ
resultArray: string[]; // ゲーム結果を格納したリスト
}
// define injection key
export const key: InjectionKey<Store<State>> = Symbol();
export const store = createStore<State>({
state: {
screenType: 0,
selectCardIndexList: [],
handChangeDone: false,
resultArray: [],
},
getters: {
getScreenType(state) {
return state.screenType;
},
getSelectCardIndexList(state) {
return state.selectCardIndexList;
},
getHandChangeDone(state) {
return state.handChangeDone;
},
getResultArray(state) {
return state.resultArray;
},
},
mutations: {
setScreenType(state, val) {
state.screenType = val;
},
setSelectCardIndexList(state, val) {
state.selectCardIndexList = val;
},
setHandChangeDone(state, val) {
state.handChangeDone = val;
},
setResultArray(state, val) {
state.resultArray = val;
},
},
actions: {
setScreenType({ commit }, val) {
commit("setScreenType", val);
},
setSelectCardIndexList({ commit }, val) {
commit("setSelectCardIndexList", val);
},
setHandChangeDone({ commit }, val) {
commit("setHandChangeDone", val);
},
setResultArray({ commit }, val) {
commit("setResultArray", val);
},
},
});
/assets/json/cardList.json
全55枚のトランプを定義したJSONファイル。
マーク(ダイヤ:s,スペード:s,ハート:h,クラブ:c,ジョーカー:joker)と数値をキーとした構造となっています。
ジョーカーの数値は100に設定おりますが、13よりも大きければ問題ありません。
[
{
"mark": "d",
"num": 1
},
{
"mark": "d",
"num": 2
},
{
"mark": "d",
"num": 3
},
{
"mark": "d",
"num": 4
},
{
"mark": "d",
"num": 5
},
{
"mark": "d",
"num": 6
},
{
"mark": "d",
"num": 7
},
{
"mark": "d",
"num": 8
},
{
"mark": "d",
"num": 9
},
{
"mark": "d",
"num": 10
},
{
"mark": "d",
"num": 11
},
{
"mark": "d",
"num": 12
},
{
"mark": "d",
"num": 13
},
{
"mark": "s",
"num": 1
},
...
{
"mark": "joker",
"num": 100
},
{
"mark": "joker",
"num": 100
}
]
/components/StartGame.vue
ゲームタイトル、ルール説明、ゲームスタートボタンを表示しているスタート画面。
ゲームスタートボタン押下でmenuIndexのstateを更新し、ゲーム画面を表示している。
<template> <div class="start-game"> <h1>ポーカーゲーム</h1> <div class="context"> <h3>ルール説明</h3> <p>・全5回ゲームを行い、それぞれのゲームの役をポイントとして表示</p> <p>・手札の交換は一回のみ可能</p> <p>・ジョーカーは2枚</p> </div> <button type="button" class="btn btn-orange" @click="startPorkerGame()"> ゲームスタート </button> </div> </template>
<script lang="ts"> import { useStore } from "vuex"; import { key } from "@/store/"; import { defineComponent } from "vue";
export default defineComponent({ components: {}, setup() { const store = useStore(key);
// method const startPorkerGame = (): void => { store.dispatch("setScreenType", 1); };
return { store, startPorkerGame, }; }, }); </script>
<style lang="scss" scoped> .start-game { display: flex; height: 100%; flex-direction: column; justify-content: center; align-items: center; .btn { margin-top: 32px; } } </style>
/components/PorkerGame.vue
ゲーム画面。
ハンドの表示や役の結果はそれぞれ別componentに定義している。
シャッフルした山札から5枚カード引き、昇順でソートしたものをハンドとして設定し、
componentに渡している。
手札の交換は選択したカードを捨て、捨てた枚数分新たに山札からカードを引いている。
次のゲームへ進むボタン押下で、再度ゲームを始められ、全5ゲームを終えると結果発表ボタンが表示され、
結果発表画面に遷移することができる。
<template>
<div class="porker-game">
<h2 class="game-count">
{{ state.currentGameCount }}/{{ maxGameCount }}回目
<span v-if="!store.getters.getHandChangeDone">交換前</span>
<span v-else>交換後</span>
</h2>
<hand-card :hand-card-list="state.handCardList" />
<hand-result :hand-card-list="state.handCardList" />
<div class="change-hand-button">
<button
v-if="!store.getters.getHandChangeDone"
type="button"
class="btn btn-orange"
@click="exchangeHand()"
>
<span v-if="store.getters.getSelectCardIndexList.length > 0"
>選択したカードを交換</span
><span v-else>交換しない</span>
</button>
<button
v-else-if="state.currentGameCount < maxGameCount"
type="button"
class="btn btn-blue"
@click="nextGame()"
>
次のゲームへ
</button>
<button
v-else
type="button"
class="btn btn-blue"
@click="showResultGame()"
>
結果発表
</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
import HandCard from "@/components/PorkerGame/HandCard.vue";
import HandResult from "@/components/PorkerGame/HandResult.vue";
import { useStore } from "vuex";
import { key } from "@/store/";
import cardListJson from "@/assets/json/cardList.json";
interface Card {
mark: string;
num: number;
}
/**
* 全カードをシャッフル
*
* @param {Object[]} cardList 全カード
* @return {Object[]} シャッフルした全カード
*/
function getShuffleCardList(cardList: Card[]) {
if (typeof window !== "undefined") {
for (let i = cardList.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[cardList[i], cardList[j]] = [cardList[j], cardList[i]];
}
}
return cardList;
}
/**
* 引数の数だけカードを絞り込む
*
* @param {Object[]} cardList 全カード
* @param {number} num 引くカードの枚数
* @return { [s: string]: any } 絞り込んだカードリスト
*/
function getFilterCardList(cardList: Card[], num: number) {
const filterCardList = Object.create(cardList).splice(0, num);
return filterCardList;
}
/**
* 数値の昇順でソート(ジョーカーは最後)
*
* @param {Object[]} handCardList ソート前の手札
* @return {Object[]} ソート後の手札
*/
function getSortHandCardList(handCardList: Card[]) {
let sortHandCardList = handCardList.slice();
sortHandCardList = sortHandCardList.sort((a: Card, b: Card) => {
if (a.num < b.num) return -1;
if (a.num > b.num) return 1;
return 0;
});
return sortHandCardList;
}
export default defineComponent({
components: { HandCard, HandResult },
setup() {
const store = useStore(key);
const state = reactive({
handCardList: [] as Card[],
currentGameCount: 1, // 現在のゲーム数
});
const maxGameCount = 5; // 最大ゲーム数
const isHandChange = false;
const handNum = 5;
let shuffleCardList: Card[] = [];
let deckCardList: Card[] = [];
/**
* ゲーム開始処理
* シャッフルした山札から5枚カードを引き、昇順にソートを行い手札として設定する。
*/
function initGame() {
shuffleCardList = getShuffleCardList(cardListJson); // 全カードをシャッフル
state.handCardList = getFilterCardList(shuffleCardList, handNum); // 引数の数だけカードを絞り込む
state.handCardList = getSortHandCardList(state.handCardList); // 数値の昇順でソート(ジョーカーは最後)
deckCardList = shuffleCardList.filter((card: Card) => {
let result = false;
const targetData = state.handCardList.find(
(handCard: Card) => handCard.num === card.num
);
if (!targetData) {
result = true;
}
return result;
}); // 山札の更新
}
initGame(); // ゲーム開始処理
/**
* ゲーム画面を表示
*/
const startPorkerGame = (): void => {
store.dispatch("setScreenType", 1);
};
/**
* 手札の交換処理
*/
const exchangeHand = (): void => {
const selectCardIndexList = store.getters.getSelectCardIndexList;
let newHand = state.handCardList.filter((data: Card, index: number) => {
return !selectCardIndexList.includes(index);
});
newHand = newHand.concat(
getFilterCardList(deckCardList, selectCardIndexList.length)
);
newHand = getSortHandCardList(newHand);
state.handCardList = newHand;
store.dispatch("setSelectCardIndexList", []);
store.dispatch("setHandChangeDone", true);
};
/**
* 次のゲームへ進む
*/
const nextGame = (): void => {
state.currentGameCount++;
initGame(); // ゲーム開始処理
store.dispatch("setHandChangeDone", false);
};
/**
* 結果画面を表示
*/
const showResultGame = (): void => {
store.dispatch("setScreenType", 2);
store.dispatch("setHandChangeDone", false);
};
return {
store,
state,
maxGameCount,
isHandChange,
handNum,
deckCardList,
startPorkerGame,
exchangeHand,
nextGame,
showResultGame,
};
},
});
</script>
<style lang="scss" scoped>
.porker-game {
.game-count {
text-align: center;
}
.change-hand-button {
display: flex;
align-items: center;
justify-content: center;
}
}
</style>
/components/PorkerGame/HandCard.vue
ハンドを画面に表示するコンポーネント。
トランプの画像をクリックで選択処理(selectCard)が実行され、
storeで管理しているselectCardIndexListを更新する。
selectCardIndexListはPorkerGame.vueで定義している手札交換処理(exchangeHand)で使用している。
<template>
<div class="hand-card">
<ul
class="card-list"
:class="{ exchanged: store.getters.getHandChangeDone }"
>
<li
v-for="(card, index) in handCardList"
:key="index"
@click="selectCard(index)"
>
<img
:class="{
'is-selected': store.getters.getSelectCardIndexList.includes(index),
}"
:src="
require(`@/assets/images/cardImages/${cardImageFileName(
card.mark,
card.num
)}.png`)
"
/>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { useStore } from "vuex";
import { key } from "@/store/";
import { defineComponent, computed } from "vue";
export default defineComponent({
components: {},
props: {
handCardList: {
type: Array,
default: () => [],
},
},
setup() {
const store = useStore(key);
// computed
const cardImageFileName = computed(() => {
return (mark: string, num: number) => {
let fileName = "";
if (mark === "joker") {
fileName = "joker";
} else {
fileName = `${mark}-${String(num)}`;
}
return fileName;
};
});
// method
const selectCard = (index: number): void => {
if (store.getters.getHandChangeDone) {
return;
}
let newSelectCardIndexList = store.getters.getSelectCardIndexList;
let targetIndex = newSelectCardIndexList.indexOf(index);
if (targetIndex >= 0) {
newSelectCardIndexList.splice(targetIndex, 1);
} else {
newSelectCardIndexList.push(index);
}
store.dispatch("setSelectCardIndexList", newSelectCardIndexList);
};
return {
store,
cardImageFileName,
selectCard,
};
},
});
</script>
<style lang="scss">
.hand-card {
.card-list {
display: flex;
align-items: center;
justify-content: center;
&.exchanged {
li {
img {
cursor: default;
}
}
}
li {
width: 150px;
&:not(:first-child) {
margin-left: 12px;
}
img {
width: 100%;
cursor: pointer;
&.is-selected {
outline: 3px solid blue;
}
}
}
}
}
</style>
/components/PorkerGame/HandResult.vue
ポーカーの役を表示するコンポーネント。
ハンドからジョーカーの数とジョーカーを抜いたハンドを定義してから、役の判定を行っている。
ジョーカーを抜いたハンドから「key:数値,value:マークの配列」のオブジェクト配列定義(※1)し、
以下の役が成立しているかを事前に判定。
■フラッシュ
ジョーカーを抜いたハンドに1種類のみのマークしか存在しないかどうか
■ロイヤルストレート(10,11,12,13,A)
ジョーカーを抜いたハンドが全て10からA内の数値かどうか
■ストレート
ジョーカーを抜いたハンドが階段状に1ずつインクリメントされているかどうか。
差が2以上あった場合でもジョーカーがある場合は、代用できるようにしている。
■フルハウス
※1のオブジェクト配列を使用し、スリーカードとワンペアの両方が成立しているかを判定。
ジョーカーが1枚ある場合はワンペアが2つ成立していればフルハウスとして判定する。
(ジョーカー2枚ある場合はフルハウスではなくフォーカードが成立するため、2枚あるときのパターンは考慮していない)
■ツーペア
※1のオブジェクト配列を使用し、ワンペアが2つ成立しているかどうかを判定。
上記の役成立結果を用いて最終的な役の結果を取得している。
<template>
<div class="hand-result">
<p>
<span v-if="store.getters.getHandChangeDone">交換後の役</span
><span v-else>現在の役</span>:{{ checkHandResult }}
</p>
</div>
</template>
<script lang="ts">
import { useStore } from "vuex";
import { key } from "@/store/";
import { defineComponent, computed, PropType } from "vue";
interface Card {
mark: string;
num: number;
}
/**
* フラッシュの役判定
*
* @param {Object[]} noJokerHandList ジョーカーを除いたハンド
* @return {boolean} true:フラッシュ役 false:フラッシュ役でない
*/
function checkIsFlash(noJokerHandList: Card[]) {
let result = false;
const set = new Set(
noJokerHandList.map((data: Card) => {
return data.mark;
})
);
if (Array.from(set).length === 1) {
result = true;
}
return result;
}
/**
* ストレートの役判定(ただし、ロイヤルストレートの形は除く)
*
* @param {Object[]} noJokerHandList ジョーカーを除いたハンド
* @param {number} jokerNum ジョーカーの数
* @return {boolean} true:ストレート役 false:ストレート役でない
*/
function checkIsStraight(noJokerHandList: Card[], jokerNum: number) {
let result = true;
let stockJoker: number = jokerNum; // ジョーカーのカウント
// ストレートかを判定
for (let i = 0; i < noJokerHandList.length - 1; i++) {
const diffNum = noJokerHandList[i + 1].num - noJokerHandList[i].num;
if (diffNum !== 1) {
if (stockJoker > 0 && diffNum > 1 && diffNum - 1 <= stockJoker) {
stockJoker -= diffNum - 1;
} else {
result = false;
}
}
}
return result;
}
/**
* ロイヤルストレートの役判定(10, 11, 12, 13, Aの形)
*
* @param {Object[]} noJokerHandList ジョーカーを除いたハンド
* @param {number} jokerNum ジョーカーの数
* @return {boolean} true:ロイヤルストレート役 false:ロイヤルストレート役でない
*/
function checkIsRoyalStraight(noJokerHandList: Card[], jokerNum: number) {
let result = false;
const handCardNum: number[] = noJokerHandList.map((data: Card) => {
return data.num;
});
const royalStraightHandNum: number[] = [1, 10, 11, 12, 13];
const diffHand: number[] = royalStraightHandNum.filter((data: number) => {
return handCardNum.indexOf(data) >= 0;
});
if (diffHand.length + jokerNum >= 5) {
result = true;
}
return result;
}
/**
* 手札内で一番重複しているカードの枚数の取得
*
* @param {Object} numKeyHand ハンドの数値をkeyとしてオブジェクト
* @return {number} 手札内で一番重複しているカードの枚数
*/
function getMaxSameCardNum(numKeyHand: { [n: number]: Card[] }) {
let maxSameCard = 0;
for (const key in numKeyHand) {
if (key === "j") {
continue;
}
if (numKeyHand[key].length > maxSameCard) {
maxSameCard = numKeyHand[key].length;
}
}
return maxSameCard;
}
/**
* フルハウスの役判定
*
* @param {Object} numKeyHand ハンドの数値をkeyとしてオブジェクト
* @param {number} jokerNum ジョーカーの数
* @return {boolean} true:フルハウス役 false:フルハウス役でない
*/
function checkIsFullHouse(
numKeyHand: { [n: number]: Card[] },
jokerNum: number
) {
let result = false;
if (jokerNum === 0) {
let threeCard = false;
let twoCard = false;
for (const key in numKeyHand) {
if (numKeyHand[key].length === 3) {
threeCard = true;
} else if (numKeyHand[key].length === 2) {
twoCard = true;
}
}
if (threeCard && twoCard) {
result = true;
}
} else if (jokerNum === 1) {
let twoCardPair = 0;
for (const key in numKeyHand) {
if (numKeyHand[key].length === 2) {
twoCardPair++;
}
}
if (twoCardPair === 2) {
result = true;
}
}
return result;
}
/**
* ツーペアの役判定
*
* @param {Object} numKeyHand ハンドの数値をkeyとしてオブジェクト
* @param {number} jokerNum ジョーカーの数
* @return {boolean} true:フルハウス役 false:フルハウス役でない
*/
function checkIsTwoPair(numKeyHand: { [n: number]: Card[] }, jokerNum: number) {
let result = false;
if (jokerNum === 0) {
let twoCardPair = 0;
for (const key in numKeyHand) {
if (numKeyHand[key].length === 2) {
twoCardPair++;
}
}
if (twoCardPair === 2) {
result = true;
}
}
return result;
}
/**
* 数字をkeyとしたオブジェクトを取得
*
* @param {Object[]} noJokerHandList ジョーカーを除いたハンド
* @return {Object} 数字をkeyとしたオブジェクト
*/
function getNumKeyHand(noJokerHandList: Card[]) {
const numKeyHand: { [n: number]: Card[] } = {};
for (let i = 0; i < noJokerHandList.length; i++) {
if (!numKeyHand[noJokerHandList[i].num]) {
numKeyHand[noJokerHandList[i].num] = [];
}
numKeyHand[noJokerHandList[i].num].push(noJokerHandList[i]);
}
return numKeyHand;
}
/**
* ポーカーの役の取得
*
* @param {Object[]} handCardList ハンド
* @return {string} ポーカーの役
*/
function getHandResult(handCardList: Card[]) {
let handResult = "役なし";
const noJokerHandList: Card[] = handCardList.filter(
(data: Card) => data.mark !== "joker"
);
const jokerNum: number = handCardList.filter(
(data: Card) => data.mark === "joker"
).length;
const numKeyHand: { [n: number]: Card[] } = getNumKeyHand(noJokerHandList);
const maxPairNum: number = getMaxSameCardNum(numKeyHand);
const isFlash = checkIsFlash(noJokerHandList);
const isRoyalStraight = checkIsRoyalStraight(noJokerHandList, jokerNum);
const isStraight = checkIsStraight(noJokerHandList, jokerNum);
const isFullHouse = checkIsFullHouse(numKeyHand, jokerNum);
const isTwoPair = checkIsTwoPair(numKeyHand, jokerNum);
// フラッシュかどうかの判定
if (isFlash) {
if (isRoyalStraight) {
// ロイヤルストレートフラッシュかどうかの判定
handResult = "ロイヤルストレートフラッシュ";
} else if (isStraight) {
// ストレートフラッシュかどうかの判定
handResult = "ストレートフラッシュ";
} else {
// フラッシュかどうかの判定
handResult = "フラッシュ";
}
} else if (isRoyalStraight || isStraight) {
// ストレートかどうかの判定
handResult = "ストレート";
} else if (maxPairNum + jokerNum === 5) {
handResult = "ファイブカード";
} else if (maxPairNum + jokerNum === 4) {
handResult = "フォーカード";
} else if (isFullHouse) {
handResult = "フルハウス";
} else if (maxPairNum + jokerNum === 3) {
handResult = "スリーカード";
} else if (isTwoPair) {
handResult = "ツーペア";
} else if (maxPairNum + jokerNum === 2) {
handResult = "ワンペア";
}
return handResult;
}
export default defineComponent({
components: {},
props: {
handCardList: {
type: Array as PropType<Array<Card>>,
default: () => [],
},
},
setup(props) {
const store = useStore(key);
const checkHandResult = computed(() => {
let handResult = getHandResult(props.handCardList); // ポーカーの役の取得
if (store.getters.getHandChangeDone) {
const newResultArray = store.state.resultArray;
newResultArray.push(handResult);
store.dispatch("setResultArray", newResultArray);
}
return handResult;
});
return {
store,
checkHandResult,
};
},
});
</script>
<style lang="scss">
.hand-result {
p {
font-size: 28px;
font-weight: bold;
color: red;
text-align: center;
}
}
</style>
/components/resultGame.vue
ゲームの結果を表示する画面。
役毎にポイントを設定し、5ゲームの集計結果を算出する。
最初に戻るボタンを押下で、スタート画面に遷移する。
<template>
<div class="result-game">
<h2>結果発表</h2>
<div class="result-list">
<ul>
<li
v-for="(result, index) in store.getters.getResultArray"
:key="index"
>
<p>{{ index + 1 }}回目: {{ result }} ({{ pointList[result] }}点)</p>
</li>
</ul>
</div>
<div class="point">
<p>点数: {{ resultPoint }}点</p>
</div>
<div class="reset">
<button type="button" class="btn btn-blue" @click="resetGame()">
最初に戻る
</button>
</div>
</div>
</template>
<script lang="ts">
import { useStore } from "vuex";
import { key } from "@/store/";
import { defineComponent } from "vue";
export default defineComponent({
components: {},
setup() {
const store = useStore(key);
let resultPoint = 0;
const pointList: { [s: string]: number } = {
ワンペア: 10,
ツーペア: 20,
スリーカード: 30,
ストレート: 40,
フラッシュ: 50,
フルハウス: 60,
フォーカード: 70,
ストレートフラッシュ: 80,
ロイヤルストレートフラッシュ: 90,
ファイブカード: 100,
役なし: 0,
};
/**
* 結果ポイントの計算
*/
function calcResultPoint() {
const resultArray = store.getters.getResultArray;
for (let i = 0; i < resultArray.length; i++) {
resultPoint += pointList[resultArray[i]];
}
}
calcResultPoint(); // 結果ポイントの計算
/**
* ゲームのリセット
*/
const resetGame = (): void => {
store.dispatch("setScreenType", 0);
store.dispatch("setResultArray", []);
};
return {
store,
resultPoint,
pointList,
resetGame,
};
},
});
</script>
<style lang="scss" scoped>
.result-game {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
.point {
font-size: 32px;
color: red;
font-weight: bold;
}
}
</style>
まとめ
工夫した点としては最大2枚のジョーカーを考慮したロジックを考えたことです。
ジョーカーを抜いたハンドとジョーカーの数を使用することで、複雑にならずにプログラムを書くことができました。
Vue3のCompositionAPIは初めて触ってみた感想としては、data,computed,methodオプションを全てsetupオプション内に記述するため、可読性の考慮が求められるなと感じました。
リアクティブな変数を定義する際にreactiveやrefを使用するなどいった変更はありますが、基本的にはVue2までの書き方と似ているため、
Vue2をやられていた方であればCompositionAPIの学習コストはそこまで高くないと感じました。
この記事に関するご相談やご質問など、お気軽にお問い合わせください。
関連する記事
-
デジタル・AI時代のロゴデザインの考え方
オピニオン/クリエイティブ
-
サービスデザイナーの改善プロセスと考え方
オピニオン/クリエイティブ
-
アクセシビリティとデザイン性の両立
オピニオン/クリエイティブ
