1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

itoアプリを作ってみたい

1
Posted at

はじめに

 突然ですが、レクリエーションで使えるアプリを作りたくなったので作りました。今回はカードゲームのitoをWebアプリケーションとしてデプロイしました。

1.概要

 まず「ito」というゲームについて、簡単に説明しておくと、
・プレイヤー全員が1から100までの数字が書かれたカードを1枚づつ引く
・その数字を、出されたお題に沿って、物や事で表現する(例えば「生き物の大きさ」の場合、1が極めて小さい生き物、100が極めて大きい生き物として考える)
・全員が表現したら、そこからはお題に沿って自由に会話できる
・数字が小さいと思う人から、順番に場へカードを出していく
という感じです。
 今回必要な要件としては、
・数字をランダムで選出する機能
・数字のブラインド機能
・表現を書き込める機能
辺りなので、これを元に実装を進めていきます。

2.実装

 今回の実装では、主にhtmlを利用して制作します。意図としては、オフラインでも動作するようにする為、githubpagesで公開する為、軽量にする為です。もっとも、私以外はダウンロードしなければオフラインで動作しないので、本末転倒ではありますが、今回は動作するアプリの制作が目的なので、意識しないものとします。
 ディレクトリ内にindex.htmlstyle.cssmain.jsを作りまして、コードをつくっていきます。実際のスクリプトはこのようになりました。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>itoアプリ</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="app">
        <header class="app-header">
            <h1 class="app-title">itoアプリ</h1>
        </header>

        <main class="app-main">
            <div class="card-scene">
                <div class="card" id="card">
                    <div class="card-face card-front">
                        <span class="number" id="number">?</span>
                    </div>
                    <div class="card-face card-back">
                        <div class="card-back-pattern"></div>
                    </div>
                </div>
            </div>

            <div class="controls">
                <button type="button" class="btn btn-draw" id="draw-btn">抽選</button>
                <button type="button" class="btn btn-blind" id="blind-btn" disabled>ブラインド</button>
            </div>
        </main>

        <footer class="app-footer">
            <div class="memo-area" id="memo-area">
                <div class="memo-display is-placeholder" id="memo-display">タップして入力...</div>
                <textarea class="memo-input" id="memo-input" placeholder="メモを入力..."></textarea>
            </div>
        </footer>
    </div>

    <script src="main.js"></script>
</body>
</html>
style.css
*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

:root {
    --bg: #100d28;
    --surface: rgba(255, 255, 255, 0.05);
    --border: rgba(255, 255, 255, 0.12);
    --text: #ddd9f5;
    --text-muted: rgba(221, 217, 245, 0.35);
    --accent-blue: #5b73d4;
    --accent-purple: #8b4ec5;
    --blue-glow: rgba(91, 115, 212, 0.45);
    --purple-glow: rgba(139, 78, 197, 0.4);
}

body {
    background-color: var(--bg);
    background-image:
        radial-gradient(ellipse at 30% 20%, rgba(60, 40, 120, 0.45) 0%, transparent 55%),
        radial-gradient(ellipse at 75% 80%, rgba(25, 15, 75, 0.45) 0%, transparent 55%);
    color: var(--text);
    font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Yu Gothic', sans-serif;
    min-height: 100svh;
}

/* ===== App Layout ===== */
.app {
    display: flex;
    flex-direction: column;
    min-height: 100svh;
    max-width: 480px;
    margin: 0 auto;
    padding: 0 20px;
}

/* ===== Header ===== */
.app-header {
    padding: 24px 0 18px;
    text-align: center;
}

.app-title {
    font-size: 1.3rem;
    font-weight: 300;
    letter-spacing: 0.35em;
    opacity: 0.9;
}

/* ===== Main ===== */
.app-main {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 40px;
    padding: 20px 0;
}

/* ===== Card ===== */
.card-scene {
    perspective: 800px;
    width: 200px;
    height: 200px;
}

.card {
    width: 100%;
    height: 100%;
    position: relative;
    transform-style: preserve-3d;
    transition: transform 0.55s cubic-bezier(0.45, 0.05, 0.55, 0.95);
}

.card.is-blind {
    transform: rotateY(180deg);
}

.card-face {
    position: absolute;
    inset: 0;
    border-radius: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
}

.card-front {
    background: var(--surface);
    border: 1px solid var(--border);
    box-shadow:
        0 4px 24px rgba(0, 0, 0, 0.5),
        inset 0 1px 0 rgba(255, 255, 255, 0.07);
}

.card-back {
    background: linear-gradient(135deg, rgba(28, 14, 68, 0.9) 0%, rgba(50, 20, 100, 0.9) 100%);
    border: 1px solid rgba(139, 78, 197, 0.35);
    transform: rotateY(180deg);
    overflow: hidden;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
}

.card-back-pattern {
    position: absolute;
    inset: 0;
    background-image:
        repeating-linear-gradient(
            45deg,
            rgba(139, 78, 197, 0.12) 0px,
            rgba(139, 78, 197, 0.12) 1px,
            transparent 1px,
            transparent 14px
        ),
        repeating-linear-gradient(
            -45deg,
            rgba(91, 115, 212, 0.12) 0px,
            rgba(91, 115, 212, 0.12) 1px,
            transparent 1px,
            transparent 14px
        );
}

/* ===== Number ===== */
.number {
    font-size: clamp(3.5rem, 18vw, 5rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    text-shadow: 0 0 28px var(--blue-glow);
    color: var(--text);
}

.number.pop {
    animation: numPop 0.38s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes numPop {
    from { transform: scale(0.55); opacity: 0; }
    to   { transform: scale(1);    opacity: 1; }
}

/* ===== Controls ===== */
.controls {
    display: flex;
    gap: 16px;
    align-items: center;
}

.btn {
    padding: 13px 26px;
    min-width: 115px;
    border-radius: 50px;
    border: 1px solid transparent;
    font-size: 0.95rem;
    font-family: inherit;
    font-weight: 500;
    letter-spacing: 0.05em;
    color: var(--text);
    cursor: pointer;
    transition: background-color 0.2s, box-shadow 0.2s, transform 0.1s, opacity 0.2s;
    -webkit-tap-highlight-color: transparent;
}

.btn:active:not(:disabled) {
    transform: scale(0.94);
}

.btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

.btn-draw {
    background: rgba(91, 115, 212, 0.18);
    border-color: rgba(91, 115, 212, 0.5);
}

.btn-draw:hover:not(:disabled) {
    background: rgba(91, 115, 212, 0.32);
    box-shadow: 0 0 18px rgba(91, 115, 212, 0.35);
}

.btn-blind {
    background: rgba(139, 78, 197, 0.18);
    border-color: rgba(139, 78, 197, 0.5);
}

.btn-blind:hover:not(:disabled) {
    background: rgba(139, 78, 197, 0.32);
    box-shadow: 0 0 18px rgba(139, 78, 197, 0.35);
}

/* ===== Footer / Memo ===== */
.app-footer {
    padding: 16px 0 28px;
}

.memo-area {
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.22);
    border: 1px solid var(--border);
    overflow: hidden;
    transition: border-color 0.2s, box-shadow 0.2s;
    min-height: 88px;
}

.memo-area:hover {
    border-color: rgba(255, 255, 255, 0.22);
}

.memo-area.is-editing {
    border-color: var(--accent-blue);
    box-shadow: 0 0 0 3px rgba(91, 115, 212, 0.2);
}

.memo-display {
    padding: 16px;
    min-height: 88px;
    cursor: pointer;
    font-size: 0.9rem;
    line-height: 1.75;
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--text);
}

.memo-display.is-placeholder {
    color: var(--text-muted);
}

.memo-area.is-editing .memo-display {
    display: none;
}

.memo-input {
    display: none;
    width: 100%;
    min-height: 88px;
    padding: 16px;
    background: transparent;
    border: none;
    outline: none;
    color: var(--text);
    font-size: 0.9rem;
    font-family: inherit;
    line-height: 1.75;
    resize: none;
}

.memo-area.is-editing .memo-input {
    display: block;
}

/* ===== Responsive ===== */
@media (max-height: 580px) {
    .app-main { gap: 24px; }
    .card-scene { width: 160px; height: 160px; }
    .app-header { padding: 14px 0 10px; }
}

main.js
'use strict';

const card      = document.getElementById('card');
const numberEl  = document.getElementById('number');
const drawBtn   = document.getElementById('draw-btn');
const blindBtn  = document.getElementById('blind-btn');
const memoArea  = document.getElementById('memo-area');
const memoDisp  = document.getElementById('memo-display');
const memoInput = document.getElementById('memo-input');

// ===== State =====
let currentNumber = null;
let isBlind       = false;
let canRedraw     = false;
// Rules:
//   - 初回抽選は常に可能
//   - 抽選後はブラインド→解除のサイクルを経ないと再抽選不可
//   - ブラインド中は再抽選不可

// ===== UI Update =====
function updateUI() {
    numberEl.textContent = currentNumber !== null ? currentNumber : '?';
    card.classList.toggle('is-blind', isBlind);

    const drawEnabled = currentNumber === null || (!isBlind && canRedraw);
    drawBtn.disabled  = !drawEnabled;

    blindBtn.disabled   = currentNumber === null;
    blindBtn.textContent = isBlind ? '解除' : 'ブラインド';
}

// ===== Actions =====
function draw() {
    currentNumber = Math.floor(Math.random() * 100) + 1;
    isBlind   = false;
    canRedraw = false;

    numberEl.classList.remove('pop');
    void numberEl.offsetWidth; // reflow to restart animation
    numberEl.classList.add('pop');

    updateUI();
}

function toggleBlind() {
    if (isBlind) {
        isBlind   = false;
        canRedraw = true;
    } else {
        isBlind = true;
    }
    updateUI();
}

drawBtn.addEventListener('click', draw);
blindBtn.addEventListener('click', toggleBlind);

// ===== Memo area =====
memoDisp.addEventListener('click', () => {
    memoArea.classList.add('is-editing');
    memoInput.focus();
    memoInput.setSelectionRange(memoInput.value.length, memoInput.value.length);
});

function closeMemo() {
    memoArea.classList.remove('is-editing');
    const val = memoInput.value;
    if (val.trim()) {
        memoDisp.textContent = val;
        memoDisp.classList.remove('is-placeholder');
    } else {
        memoDisp.textContent = 'タップして入力...';
        memoDisp.classList.add('is-placeholder');
    }
}

memoInput.addEventListener('blur', closeMemo);

memoInput.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') memoInput.blur();
});

memoInput.addEventListener('input', () => {
    memoInput.style.height = 'auto';
    memoInput.style.height = Math.max(88, memoInput.scrollHeight) + 'px';
});

// ===== Init =====
updateUI();

これでもうアプリ自体はできました。

スクリーンショット 2026-05-29 153328.png

3.デプロイ

 さて、これをgithubに上げていくわけですが、今回高々3ファイルしかないので、webに直接ぶん投げます。そして、プロジェクトのSettingからPagesを選択し、ブランチを選べば、もう公開できます。1回更新すれば、もうリンクが表示されていると思います。

おわりに

 こんな感じで、自分の作りたいアプリを公開してみました、今回はgithubのPagesを使いましたが、例えばReactとかで作った場合は、他のプラットフォーム(Vercelなど)でないと不可能なので気を付けましょう。このようなWebアプリ制作において、この記事を、少しでも参考にしていただけたら幸いです。
 今回制作したWebアプリ↓
https://tangentkoh.github.io/application_ito/

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?