普通のサイコロ転がしです

機能は特にありません。サイコロを転がすボタン・サイコロの目を予想する入力ボタン・当たりの演出だけです。

このサイコロ転がしは、ゲームというよりはプログラムの全体コード・部分コード等を参考にしていただくのが本来の目的です

サイコロ画像に付きましてはフリー素材からお借りした画像になります。なので独自に開発してWEBで公開する場合は画像使用の配慮が必要かもしれません。

HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>サイコロ</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="startTitle">ふつうのさいころ</div>
<div id="game-area">
    <canvas id="diceCanvas"width="600"height="400"></canvas>
    <!-- キャンバス内上部の操作エリア -->
    <div id="dice-controls">
    <!-- 1行目:ボタン+結果 -->
    <div class="control-row">
        <button id="rollBtn">サイコロを振る</button>

        <div id="result">結果:</div>
    </div>
    <!-- 2行目:予想数字+判定 -->
        <div class="guess-row">
        <label for="targetNumber">
            あなたが出したい数字
        </label>

        <input
            type="number"
            id="targetNumber"
            min="1"
            max="6"
            placeholder="1~6"
        />
            <div id="judgement"></div>
        </div>
    </div>   
</div>
<script src="main.js"></script>
</body>
</html>
JS
const canvas = document.getElementById("diceCanvas");
const ctx = canvas.getContext("2d");

const rollBtn = document.getElementById("rollBtn");
const result = document.getElementById("result");
const targetNumber = document.getElementById("targetNumber");
const judgement = document.getElementById("judgement");

const bgImg = new Image();
const centerDiceImg = new Image();

bgImg.src = "img/background.png";//ファイル構成は環境に応じて
centerDiceImg.src = "img/center-dice.png";

let bgLoaded = false;
let diceLoaded = false;

bgImg.onload = function () {
    bgLoaded = true;
    drawStartScreen();
};

centerDiceImg.onload = function () {
    diceLoaded = true;
    drawStartScreen();
};

function drawStartScreen() {

    if (!bgLoaded || !diceLoaded) return;

    // 背景
    ctx.drawImage(
        bgImg,
        0,
        0,
        canvas.width,
        canvas.height
    );
    // 中央の薄いサイコロ
    const centerSize = 240;

    ctx.save();

    ctx.globalAlpha = 0.12;

    ctx.drawImage(
        centerDiceImg,

        // 元画像800×600から600×600を切り出す
        100,
        0,
        600,
        600,

        // Canvas中央
        (canvas.width - centerSize) / 2,
        (canvas.height - centerSize) / 2,

        centerSize,
        centerSize
    );

    ctx.restore();
}

centerDiceImg.src = "img/dice1.png";

bgImg.onerror = function () {

    console.error(
        "background.png の読み込みに失敗しました"
    );
};

// 紙吹雪
let confetti = [];

function createConfetti() {

    confetti = [];

    for (let i = 0; i < 60; i++) {

        confetti.push({

            x: Math.random() * canvas.width,

            y: -10 - Math.random() * 80,

            size: 4 + Math.random() * 5,

            vx: -1.5 + Math.random() * 3,

            vy: 1 + Math.random() * 2,

            gravity: 0.03 + Math.random() * 0.04,

            rotation:
                Math.random() * Math.PI * 2,

            rotationSpeed:
                -0.08 + Math.random() * 0.16,

            swing:
                Math.random() * Math.PI * 2,

            swingSpeed:
                0.03 + Math.random() * 0.05

        });
    }
}

// 紙吹雪を描画
function drawConfetti() {

    for (const p of confetti) {

        // 横方向に少しヒラヒラ
        p.swing += p.swingSpeed;
        p.x += p.vx +Math.sin(p.swing) * 0.5;
        p.vy += p.gravity;
        p.y += p.vy;
        p.rotation += p.rotationSpeed;

        // 描画
        ctx.save();
        ctx.translate(p.x,p.y);
        ctx.rotate(p.rotation);
        ctx.fillStyle = `hsl(${Math.random() * 360}, 80%, 55%)`;
        ctx.fillRect(-p.size / 2,-p.size / 2,p.size,p.size);
        ctx.restore();
    }
    // 画面外を削除
    confetti =confetti.filter(p => p.y < canvas.height + 20);
}
// 設定
// サイコロの大きさ
const DICE_SIZE = 100;

// キャンバス下端からの余白
const BOTTOM_MARGIN = 25;

// サイコロ画像
const diceImages = [];

    for (let i = 1; i <= 6; i++) {

        const img = new Image();

        img.src = `img/dice${i}.png`;

        diceImages.push(img);
    }

// サイコロボタン
rollBtn.addEventListener("click", function (e) {

    // ボタン連打禁止
    if (this.disabled) {
        e.preventDefault();
        e.stopImmediatePropagation();
        return;
    }

    this.disabled = true;

    result.textContent = "結果:";
    judgement.textContent = "";
    judgement.className = "";

    confetti = [];

    playThrowAnimation();
});

// 背景描画
function drawBackground() {

    if (!bgImg.complete) {
        return;
    }
    // 背景画像
    ctx.drawImage(bgImg,0,0,canvas.width,canvas.height);

    // 中央の薄いサイコロ
    if (centerDiceImg.complete) {

        const centerSize = 240;

        ctx.save();

        // 薄く表示
        ctx.globalAlpha = 0.12;

        ctx.drawImage(
        centerDiceImg,
        // 元画像 800×600
        // 600×600 の正方形を切り出す
        100,
        0,
        600,
        600,
        // Canvas中央
        (canvas.width - centerSize) / 2,
        (canvas.height - centerSize) / 2,
        // 表示サイズ
        centerSize,
        centerSize
    );

    ctx.restore();
    }
}

// 投げる前の「手」の演出
function playThrowAnimation() {

    const startTime = performance.now();

    const duration = 600;

    function animateHand(now) {

        const elapsed = now - startTime;
        let t = elapsed / duration;

        if (t > 1) {
            t = 1;
        }

        // 背景
        drawBackground();

        // 手の位置
        const x = canvas.width - 45;
        const y = 55 + Math.sin(t * Math.PI * 4) * 8;
        // 手の角度
        const angle = Math.PI / 2 + Math.sin(t * Math.PI * 4) * 0.18;
        // 手の絵文字
        ctx.save();
        // 手の位置
        ctx.translate(x, y);
        // 左右反転
        ctx.scale(-1, 1);
        // 回転軸を右側(手元側)へ
        ctx.translate(15, 0);
        // 上下の振り
        ctx.rotate(angle);
        // 元の位置へ戻す
        ctx.translate(-15, 0);
        ctx.font = "42px Arial";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText("✋",0,0);
        ctx.restore();
        // 手の演出終了
        if (t >= 1) {

            rollDice();
            return;
        }
        requestAnimationFrame(
            animateHand
        );
    }

    requestAnimationFrame(
        animateHand
    );
}

// 角度を -π ~ +π に整理
function normalizeAngle(angle) {

    const twoPi = Math.PI * 2;

    angle = ((angle + Math.PI) % twoPi + twoPi) % twoPi - Math.PI;

    return angle;
}
// イージング
// 最後に向かって非常にゆっくり
function easeOutCubic(t) {

    return 1 - Math.pow(1 - t, 3);
}

let x = canvas.width - DICE_SIZE - 20;
let y = 20;

// 地面の高さ
const groundY = canvas.height - DICE_SIZE - BOTTOM_MARGIN;

// サイコロを振る
function rollDice() {

// 最終結果
const finalNumber =
    Math.floor(Math.random() * 6) + 1;

// 最初の目
let currentNumber =
    Math.floor(Math.random() * 6) + 1;

// 初期位置
let x = canvas.width - DICE_SIZE - 20;
let y = 20;

// 移動
let vx = -5 - Math.random() * 2;
let vy = 0;
const gravity = 0.35;

// 回転
let angle = 0;
let rotationSpeed = 0.25 + Math.random() * 0.15;

// 状態
let state = "air";
let confettiStarted = false;
let bounceCount = 0;
let bounceVelocity = 0;
const bounceGravity = 0.45;

// 目の切り替え
let faceTimer = 0;
let faceInterval = 3;

// 最終停止用
let settleStartTime = 0;
let settleStartAngle = 0;
let settleEndAngle = 0;

// 最後は1.5秒かけて
// ゆっくり正立させる
const settleDuration = 1500;

// 正立後の小さな揺れ
const wobbleDuration = 350;

// サイコロ描画
function drawDice(number, drawAngle) {

const img = diceImages[number - 1];

    if (!img) { return;}

const size = DICE_SIZE;

    ctx.save();
    // サイコロの中心
    ctx.translate( x + size / 2, y + size / 2);
    // 回転
    ctx.rotate(drawAngle);
    // サイコロの影
    ctx.shadowColor = "rgba(0, 0, 0, 0.35)";
    ctx.shadowBlur = 8;
    ctx.shadowOffsetX = 5;
    ctx.shadowOffsetY = 6;
    // サイコロ描画
    ctx.drawImage( img,100, 0,600, 600,-size / 2,-size / 2,size,size);
    ctx.restore();
}

// アニメーション
function animate(now) {

    ctx.clearRect(0,0,canvas.width,canvas.height);   
     // 毎フレーム背景を最初に描画
    drawBackground();
    // 空中
    if (state === "air") {

        // 重力
        vy += gravity;
        // 移動
        x += vx;
        y += vy;
        // 回転
        angle += rotationSpeed;
        // 目を変更
        faceTimer++;

    if (faceTimer >= faceInterval) {

        currentNumber = Math.floor(Math.random() * 6) + 1;faceTimer = 0;
        }
        // 着地
        if (y >= groundY) {

        y = groundY;

        // 小さく跳ねる
        bounceVelocity = -5.0;
        bounceCount = 0;
        state = "bounce";
        vx *= 0.45;
        rotationSpeed *= 0.55;
        }
    }

else if (state === "bounce") {

    // 上下方向
    bounceVelocity += bounceGravity;

    y += bounceVelocity;

    // 回転も少し続ける
    angle += rotationSpeed * 0.5;

    // 地面に戻った
    if (y >= groundY) {

        y = groundY;

        bounceCount++;

    // もう一度だけ小さく跳ねる
    if (bounceCount < 2) {

        bounceVelocity = -3.0;

    } else {

        // バウンド終了
        state = "ground";
        }
    }
    // 空中でも少し回転
    angle += rotationSpeed * 0.5;
}
// 地面を転がる
else if (state === "ground") {
        // 横移動
        x += vx;
        // 回転
        angle += rotationSpeed;
        // 横方向の摩擦
        vx *= 0.96;
        // 回転減速
        rotationSpeed *= 0.985;
        // 目を変更
        faceTimer++;

    if (faceTimer >= faceInterval) {

        currentNumber = Math.floor(Math.random() * 6) + 1;

        faceTimer = 0;

        // 目の変化を徐々に遅くする
        faceInterval += 0.7;
        }
    // 最終停止へ
    if (
        Math.abs(vx) < 0.5 &&rotationSpeed < 0.045
        ) {

        state = "settling";
        // 最終結果
        currentNumber = finalNumber;
        // 現在角度を整理
        settleStartAngle = normalizeAngle(angle);
        // 正立に近い角度へ補正
    if (
        Math.abs(settleStartAngle) >
         Math.PI / 2
        ) {
        settleStartAngle = settleStartAngle > 0? Math.PI - settleStartAngle: -Math.PI - settleStartAngle;
        }
        // 最終角度
        settleEndAngle = 0;

        // 開始時間
        settleStartTime = now;
        }
    }
// 最後のゆっくり停止
else if (state === "settling") {

    const elapsed =
        now - settleStartTime;
    // ① 1.5秒かけてゆっくり正立
    if (elapsed < settleDuration) {

        let t = elapsed / settleDuration;

        if (t > 1) {
            t = 1;
        }

    const eased = easeOutCubic(t);
        angle = settleStartAngle +(settleEndAngle - settleStartAngle) * eased;
        currentNumber =finalNumber;
    }
    // 最後に小さく揺れる
    else {

    const wobbleElapsed = elapsed - settleDuration;

        let t = wobbleElapsed / wobbleDuration;

        if (t > 1) {
            t = 1;
        }

    const wobbleAngle = Math.sin(t * Math.PI * 2) *(1.5 * Math.PI / 180);

        angle = settleEndAngle + wobbleAngle;
        currentNumber = finalNumber;
        // 完全停止
        if (t >= 1) {
            angle = settleEndAngle;
            // ここで結果を確定表示
            result.textContent = `結果:${finalNumber}`;
            state = "done";
            rollBtn.disabled = false; //連打禁止解除
        }
    }
}

// 完全停止
if (state === "done") {

    // 最終結果
    drawDice(finalNumber,0);
    // あたり・はずれ
    const target = Number(targetNumber.value);

    if (
        target >= 1 &&
        target <= 6
    ) {

    if (
        target === finalNumber
    ) {
        judgement.textContent = "🎯 あたり おめでとう!";
        judgement.className = "hit";
        // 紙吹雪を開始
    if (!confettiStarted) {
        createConfetti();
        confettiStarted = true;
    }
    } else {
        judgement.textContent = "はずれ";
        judgement.className = "miss";
    }} else {
        judgement.textContent = "";
        judgement.className = "";
    }
    // 紙吹雪を描画
    drawConfetti();
    // 紙吹雪が残っている間は
    // アニメーションを継続
    if (confetti.length > 0) {
        requestAnimationFrame(animate);
    }
    return;
}
    // 描画
    if (state === "settling") {
        // 最終結果だけを表示
        drawDice(finalNumber,angle);
    } else {
        // 転がっている途中
        drawDice(currentNumber,angle);
    }
// 紙吹雪
drawConfetti();
        // 次のフレーム
        requestAnimationFrame(
            animate
        );
    }
    // スタート
    requestAnimationFrame(
        animate
    );
}
CSS
body {margin: 0;background: #979797;text-align: center;font-family: sans-serif;}

#startTitle {margin: 10px auto 0; color: #dfbdff; font-size: 32px; font-weight: bold; text-align: center;
text-shadow: 0 0 8px rgba(37, 37, 37, 0.911);}

#game-area {position: relative; margin: 6px auto; width: 600px; height: 400px; border-radius: 22px;
overflow: hidden; box-shadow:0 8px 20px rgba(0,0,0,0.18),0 2px 5px rgba(0,0,0,0.18);}

#game-area::before {content: ""; position: absolute;inset: 0;padding: 4px;background:linear-gradient(90deg,#ff6b6b,#ffd93d,
#6bcB77,#4d96ff,#c77dff,#ff6b6b);background-size: 300% 300%;animation:borderMove 5s linear infinite;border-radius: 22px;
mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask-composite: exclude;pointer-events: none;}
@keyframes borderMove {0% {background-position: 0% 50%;}50% {background-position: 100% 50%;}100% {background-position: 0% 50%;}}

#rollBtn {background-color: #ccc;width : 200px;padding: 10px 20px;font-size: 1rem;text-align: center;color: #fff;
text-shadow: 1px 1px 1px #000;border-radius: 10px;background-color: rgb(118, 37, 250);background-image: linear-gradient(
to top left, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2) 30%, rgba(0, 0, 0, 0)       );
box-shadow:inset 2px 2px 3px rgba(255, 255, 255, 0.6),inset -2px -2px 3px rgba(0, 0, 0, 0.6);}

#rollBtn:hover {background: #add8fc;transform: translateY(-1px);box-shadow: 0 4px 8px rgba(0, 0, 0, 0.20);}
#rollBtn:active {transform: translateY(2px);box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);}
#diceCanvas {display: block;width: 600px;height: 400px;border-radius: 19px;}
#dice-controls {position: absolute;top: 15px;left: 40px;z-index: 10;text-align: left;}
.control-row {display: flex;align-items: center;gap: 15px;}
.guess-row {display: flex;align-items: center;gap: 8px; color: #8d6417;
text-shadow: 0 0 4px rgba(204, 204, 204, 0.911); margin-top: 10px;font-weight: bold; font-size: 18px;}

#targetNumber {width: 70px;height: 30px;padding: 2px 6px;box-sizing: border-box;font-size: 16px;text-align: center;
border: 2px solid #cf2172;border-radius: 6px;background: #fff;}

#result {background-color: #ccc; width: 60px; padding: 10px 20px; font-size: 1rem; text-align: left; color: #fff;
text-shadow: 1px 1px 1px #000; border-radius: 10px; background-color: rgb(40, 94, 9); background-image: linear-gradient(to top left,
rgba(0, 0, 0, 0.2),rgba(0, 0, 0, 0.2) 30%,rgba(0, 0, 0, 0)); box-shadow:inset 2px 2px 3px rgba(255, 255, 255, 0.6),inset -2px -2px 3px rgba(0, 0, 0, 0.6);}

#judgement {margin-left: 8px;font-size: 18px;font-weight: bold;min-width: 70px;}
#judgement {margin-left: 8px;font-size: 18px;font-weight: bold;min-width: 80px;}
#judgement.hit {color: red;animation: hitBlink 0.5s infinite;}

@keyframes hitBlink {0% {opacity: 1;}50% {opacity: 0.2;}100% {opacity: 1;}}

#judgement.miss {color: blue;animation: none;}

@media screen and (max-width: 650px)
{#game-area {width: calc(100vw - 20px);height: auto;aspect-ratio: 3 / 2;margin: 15px auto;border-radius: 18px;}
#diceCanvas {width: 100%;height: 100%;border-radius: 15px;}

#dice-controls {top: 12px;left: 20px;}
.control-row {gap: 10px;}
.guess-row {gap: 6px;margin-top: 8px;font-size: 14px;}
#rollBtn { padding: 8px 14px;font-size: 15px;}
#result {min-width: 70px;padding: 6px 9px;font-size: 17px;}
#targetNumber {width: 60px;height: 30px;font-size: 16px;}
#judgement {font-size: 17px;min-width: 70px;}}

@media screen and (max-width: 480px) {
body {overflow-x: hidden;}
#game-area {width: calc(100vw - 10px);margin: 8px auto;border-width: 2px;border-radius: 15px;}
#diceCanvas {border-radius: 13px;}
#dice-controls {top: 8px;left: 10px;}
.control-row {gap: 7px;}
#rollBtn {padding: 7px 10px;font-size: 13px;border-radius: 8px;min-height: 38px;}
#result {min-width: 65px;padding: 5px 7px;font-size: 15px;border-radius: 7px;}
.guess-row {gap: 5px;margin-top: 7px;font-size: 12px;white-space: nowrap;}
#targetNumber {width: 50px;height: 32px;padding: 2px;font-size: 16px;border-radius: 6px;}
#judgement {margin-left: 3px;font-size: 15px;min-width: 55px;}}

以上です・・

デモサイトは紹介コードとは多少違います。1個・2個のサイコロを振るのボタン、当たりの場合はサイコロがアニメーションする、ボタン類の配置、背景とタイトルなどです。よろしかったら見てください。

≪≪サイコロ転がしデモサイト≫≫