お正月っぽい “お年玉抽選会” を、ブラウザだけで動くガチャとして作ってみました🎰✨
アプリのガチャ風に、
…を全部入れています!
🎯 景品(例)
🎰 ガチャ演出
📌 実装の特徴
crypto.getRandomValues() を使って偏りにくくotoshidama_gacha.html にするJS内のここ👇(配列)を編集するだけです。
const prizes = [
{ key:"1", name:"1等", amount:10000, prob: 1, theme:"gold" },
{ key:"2", name:"2等", amount: 5000, prob: 6, theme:"purple"},
{ key:"3", name:"3等", amount: 2000, prob: 23, theme:"blue" },
{ key:"4", name:"4等", amount: 500, prob: 70, theme:"green" }
];
amount → 金額prob → 当選確率(合計100にする)name → 表示名(例:「特賞」などにも変更可)このへんで「間」を作っています👇(数字を少し大きくすると長くなる)
await tickSequence(820 + rand01()*420);
「当たった感」を演出で増やすと、体感がガラッと変わります😆
長いので折りたたみ。開いて全部コピペしてください👇
<details> <summary><strong>✅ ここをクリックでコード表示(全部コピーOK)</strong></summary>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>お年玉ガチャ抽選会</title>
<style>
:root{
--bg1:#0b1020; --bg2:#1a0b2a;
--text:#e8eefc; --muted:#a7b3d6;
--ring: 0 0 0 3px rgba(124,58,237,.25);
--panel: rgba(15,23,42,.72);
--border: rgba(255,255,255,.10);
}
*{box-sizing:border-box}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Hiragino Kaku Gothic ProN", "Noto Sans JP", "Yu Gothic", Arial, sans-serif;
color:var(--text);
min-height:100vh;
background:
radial-gradient(1200px 600px at 20% 10%, rgba(124,58,237,.35), transparent 60%),
radial-gradient(900px 500px at 80% 20%, rgba(34,197,94,.25), transparent 55%),
radial-gradient(800px 500px at 50% 90%, rgba(239,68,68,.18), transparent 55%),
linear-gradient(180deg, var(--bg1), var(--bg2));
overflow-x:hidden;
}
.wrap{ max-width:980px; margin:0 auto; padding:24px 16px 40px; }
header{ display:flex; justify-content:space-between; align-items:flex-start; gap:12px; margin-bottom:16px; }
h1{ font-size: clamp(20px, 2.6vw, 30px); margin:0; letter-spacing:.02em; }
.subtitle{ margin-top:6px; color:var(--muted); font-size:14px; line-height:1.5; }
.grid{ display:grid; grid-template-columns: 1.05fr .95fr; gap:14px; }
@media (max-width: 860px){ .grid{grid-template-columns:1fr} }
.card{
background: var(--panel);
border:1px solid var(--border);
border-radius:18px;
box-shadow: 0 18px 40px rgba(0,0,0,.35);
overflow:hidden;
}
.card .hd{
padding:14px 14px 0;
display:flex; justify-content:space-between; align-items:center; gap:10px;
}
.badge{
font-size:12px; padding:6px 10px; border-radius:999px;
border:1px solid rgba(255,255,255,.14);
color:var(--muted); background: rgba(255,255,255,.04);
white-space:nowrap;
}
.body{ padding:14px; }
.stage{
position:relative;
min-height:330px;
border-radius:16px;
background:
radial-gradient(700px 280px at 50% 0%, rgba(255,255,255,.12), transparent 60%),
linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
border:1px solid rgba(255,255,255,.10);
overflow:hidden;
display:flex;
flex-direction:column;
gap:12px;
align-items:center;
justify-content:center;
padding:16px;
}
.stage::after{
content:"";
position:absolute; inset:-2px;
background: radial-gradient(600px 220px at 50% 0%, rgba(124,58,237,.25), transparent 65%);
opacity:.8;
pointer-events:none;
filter: blur(8px);
transform: translateZ(0);
}
.fxLayer{ position:absolute; inset:0; z-index:5; pointer-events:none; overflow:hidden; }
.resultBox{
position:relative; z-index:2;
width:min(560px, 100%);
text-align:center;
padding:16px 14px;
border-radius:16px;
background: rgba(2,6,23,.55);
border:1px solid rgba(255,255,255,.12);
backdrop-filter: blur(10px);
transition: transform .2s ease, box-shadow .2s ease;
}
.resultTitle{ font-size:14px; color:var(--muted); margin:0 0 8px; letter-spacing:.12em; text-transform:uppercase; }
.rank{ font-weight:900; font-size: clamp(26px, 4vw, 42px); margin:0; line-height:1.1; }
.amount{ margin:10px 0 0; font-size: clamp(20px, 3.2vw, 34px); font-weight:900; }
.hint{ margin:10px 0 0; color:var(--muted); font-size:13px; }
.btnRow{ display:flex; gap:10px; flex-wrap:wrap; margin-top:12px; justify-content:center; }
button{
appearance:none; border:none; cursor:pointer;
border-radius:14px; padding:12px 14px;
font-weight:900; color:var(--text);
background: linear-gradient(135deg, rgba(124,58,237,.95), rgba(34,197,94,.85));
box-shadow: 0 14px 30px rgba(0,0,0,.25);
transition: transform .12s ease, filter .12s ease, box-shadow .12s ease;
letter-spacing:.03em;
}
button:hover{ transform: translateY(-1px); filter: brightness(1.05); }
button:active{ transform: translateY(1px); filter: brightness(.98); }
button:focus-visible{ outline:none; box-shadow: var(--ring), 0 14px 30px rgba(0,0,0,.25); }
button.secondary{
background: rgba(255,255,255,.08);
border:1px solid rgba(255,255,255,.14);
box-shadow:none;
color:var(--text);
}
button.secondary:hover{ background: rgba(255,255,255,.10); }
button[disabled]{ opacity:.55; cursor:not-allowed; transform:none; filter:none; }
.toggle{ display:flex; align-items:center; gap:8px; color:var(--muted); font-size:13px; user-select:none; cursor:pointer; }
.toggle input{ transform: translateY(1px); }
.rows{ display:flex; flex-direction:column; gap:12px; }
.row{
display:grid; grid-template-columns: 1fr auto;
align-items:center; gap:10px;
padding:10px 10px;
border-radius:14px;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.04);
}
.row .left{ display:flex; flex-direction:column; gap:4px; }
.row .label{ display:flex; align-items:baseline; justify-content:space-between; gap:10px; font-weight:900; }
.row small{ color:var(--muted); font-size:12px; line-height:1.4; }
input[type="range"]{ width: 220px; }
.pct{ font-variant-numeric: tabular-nums; color:var(--muted); font-weight:800; }
.warn{ color:#fbbf24; font-size:12px; margin-top:10px; line-height:1.4; }
.ok{ color:#86efac; font-size:12px; margin-top:10px; line-height:1.4; }
.history{
max-height: 210px;
overflow:auto;
border-radius:14px;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.03);
padding:10px;
}
.histItem{
display:flex; justify-content:space-between; gap:10px;
padding:8px 8px;
border-radius:12px;
border:1px solid rgba(255,255,255,.08);
background: rgba(2,6,23,.35);
margin-bottom:8px;
font-size:13px;
}
.histItem:last-child{ margin-bottom:0; }
.histLeft{ display:flex; flex-direction:column; gap:2px; }
.histRank{ font-weight:900; }
.histTime{ color:var(--muted); font-size:12px; }
.sumRow{
display:flex; justify-content:space-between; gap:10px;
margin-top:10px;
padding:10px 10px;
border-radius:14px;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.04);
font-weight:900;
}
.gachaWrap{
position:relative;
z-index:3;
width:min(560px, 100%);
display:grid;
grid-template-columns: 1fr 170px;
gap:12px;
align-items:center;
}
@media (max-width:520px){
.gachaWrap{ grid-template-columns:1fr; }
}
.machine{
position:relative;
border-radius:16px;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.04);
padding:12px;
overflow:hidden;
}
.machine::before{
content:"";
position:absolute; inset:-2px;
background: radial-gradient(380px 180px at 30% 10%, rgba(255,255,255,.10), transparent 60%);
opacity:.9; pointer-events:none;
}
.machineInner{ position:relative; display:flex; align-items:center; justify-content:space-between; gap:10px; z-index:2; }
.glass{
width:84px; height:84px;
border-radius:999px;
border:1px solid rgba(255,255,255,.16);
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.26), rgba(255,255,255,.06) 55%, rgba(2,6,23,.35) 100%);
box-shadow: inset 0 0 30px rgba(255,255,255,.06);
position:relative;
overflow:hidden;
flex:0 0 auto;
}
.balls{ position:absolute; inset:0; }
.ball{
position:absolute;
width:14px; height:14px;
border-radius:999px;
background: rgba(255,255,255,.82);
opacity:.85;
filter: drop-shadow(0 3px 6px rgba(0,0,0,.25));
}
.spinning .ball{
animation: swirl 650ms ease-in-out infinite;
}
@keyframes swirl{
0%{ transform: translate(0,0) scale(1); opacity:.7;}
50%{ transform: translate(var(--dx), var(--dy)) scale(1.15); opacity:1;}
100%{ transform: translate(0,0) scale(1); opacity:.7;}
}
.slot{
flex:1;
min-height:56px;
border-radius:14px;
border:1px dashed rgba(255,255,255,.14);
display:flex;
align-items:center;
justify-content:center;
text-align:center;
padding:8px;
color:var(--muted);
font-weight:900;
letter-spacing:.08em;
background: rgba(2,6,23,.20);
}
.slot .ticker{ display:inline-block; font-variant-numeric: tabular-nums; }
.slot.rolling .ticker{ animation: ticker 120ms linear infinite; }
@keyframes ticker{
0%{ transform: translateY(0); opacity:.8; }
50%{ transform: translateY(-2px); opacity:1; }
100%{ transform: translateY(0); opacity:.8; }
}
.handlePanel{
position:relative;
border-radius:16px;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.04);
padding:12px;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:10px;
overflow:hidden;
}
.handlePanel::before{
content:"";
position:absolute; inset:-2px;
background: radial-gradient(220px 140px at 60% 20%, rgba(124,58,237,.18), transparent 70%);
pointer-events:none;
}
.handle{
position:relative;
width:110px; height:110px;
display:grid;
place-items:center;
z-index:2;
user-select:none;
}
.hub{
width:46px; height:46px;
border-radius:999px;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.40), rgba(255,255,255,.08) 60%, rgba(2,6,23,.40));
border:1px solid rgba(255,255,255,.18);
box-shadow: inset 0 0 18px rgba(255,255,255,.08), 0 10px 22px rgba(0,0,0,.25);
position:relative;
}
.arm{
position:absolute;
width:60px; height:10px;
background: rgba(255,255,255,.14);
border:1px solid rgba(255,255,255,.16);
border-radius:999px;
top:50%;
left:50%;
transform-origin: 8px 50%;
transform: translate(-8px,-50%) rotate(0deg);
box-shadow: 0 8px 18px rgba(0,0,0,.20);
}
.knob{
position:absolute;
width:18px; height:18px;
border-radius:999px;
right:-9px; top:50%;
transform: translateY(-50%);
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.55), rgba(255,255,255,.10));
border:1px solid rgba(255,255,255,.18);
}
.turning .arm{ animation: turn 1.15s cubic-bezier(.2,.9,.2,1) both; }
@keyframes turn{
0% { transform: translate(-8px,-50%) rotate(0deg); }
25% { transform: translate(-8px,-50%) rotate(110deg); }
55% { transform: translate(-8px,-50%) rotate(250deg); }
100% { transform: translate(-8px,-50%) rotate(360deg); }
}
.handleHint{
font-size:12px;
color:var(--muted);
z-index:2;
text-align:center;
line-height:1.35;
}
.veil{
position:absolute; inset:0;
background: linear-gradient(180deg, rgba(2,6,23,.05), rgba(2,6,23,.25));
opacity:0;
pointer-events:none;
transition: opacity .2s ease;
z-index:4;
}
.veil.on{ opacity:1; }
.shake{ animation: shake .6s ease both; }
@keyframes shake{
0%,100%{ transform:translateX(0); }
20%{ transform:translateX(-6px); }
40%{ transform:translateX(6px); }
60%{ transform:translateX(-4px); }
80%{ transform:translateX(4px); }
}
.glow1{ animation: glow1 1.1s ease-in-out both; }
@keyframes glow1{
0%{ box-shadow: 0 0 0 rgba(255,255,255,0); transform: scale(.98); }
30%{ box-shadow: 0 0 40px rgba(255,215,0,.35); transform: scale(1.02); }
100%{ box-shadow: 0 0 18px rgba(255,215,0,.22); transform: scale(1); }
}
.glow2{ animation: glow2 .95s ease-in-out both; }
@keyframes glow2{
0%{ box-shadow: 0 0 0 rgba(255,255,255,0); transform: scale(.99); }
35%{ box-shadow: 0 0 36px rgba(99,102,241,.28); transform: scale(1.015); }
100%{ box-shadow: 0 0 16px rgba(99,102,241,.18); transform: scale(1); }
}
.pop{ animation: pop .55s cubic-bezier(.2,.9,.2,1) both; }
@keyframes pop{
0%{ transform: scale(.92); opacity:.4; }
60%{ transform: scale(1.04); opacity:1; }
100%{ transform: scale(1); }
}
.confetti{
position:absolute; top:-12px;
width:10px; height:18px;
opacity:.95;
border-radius:3px;
transform: rotate(var(--rot));
left: var(--x);
animation: fall var(--dur) linear forwards;
filter: drop-shadow(0 4px 8px rgba(0,0,0,.25));
z-index:6;
}
@keyframes fall{
to{ transform: translateY(120%) rotate(calc(var(--rot) + 220deg)); opacity:1; }
}
.spark{
position:absolute;
width:10px; height:10px;
left: var(--x); top: var(--y);
border-radius:999px;
background: rgba(255,255,255,.9);
box-shadow: 0 0 18px rgba(255,255,255,.6);
animation: spark 0.75s ease-out forwards;
z-index:6;
}
@keyframes spark{
0%{ transform: scale(.2); opacity:0; }
35%{ transform: scale(1.2); opacity:1; }
100%{ transform: translateY(-30px) scale(.1); opacity:0; }
}
.burst{
position:absolute;
left:50%; top:50%;
width:8px; height:8px;
border-radius:999px;
background: rgba(255,255,255,.85);
transform: translate(-50%,-50%);
animation: burst 650ms ease-out forwards;
opacity:.95;
z-index:6;
}
@keyframes burst{
0%{ transform: translate(-50%,-50%) scale(.2); opacity:0; }
25%{ opacity:1; }
100%{ transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(.05); opacity:0; }
}
.chips{ display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; justify-content:center; }
.chip{
font-size:12px; color:var(--muted);
border:1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.04);
padding:6px 10px; border-radius:999px;
}
.footerNote{ margin-top:10px; color:var(--muted); font-size:12px; line-height:1.5; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<h1>🎍 お年玉ガチャ抽選会</h1>
<div class="subtitle">ハンドルを回してガチャ感UP!判定までの“ワクワク時間”も追加済み。</div>
</div>
<div class="badge">1ファイル / HTML+CSS+JS</div>
</header>
<div class="grid">
<section class="card">
<div class="hd">
<div class="badge">ガチャガチャ演出</div>
<label class="toggle" title="音をON/OFF(初回はボタン押下で音が有効になります)">
<input id="soundToggle" type="checkbox" checked />
🔊 サウンド
</label>
</div>
<div class="body">
<div id="stage" class="stage" aria-live="polite">
<div class="veil" id="veil"></div>
<div class="fxLayer" id="fxLayer"></div>
<div class="gachaWrap">
<div class="machine" aria-hidden="true">
<div class="machineInner">
<div class="glass" id="glass">
<div class="balls" id="balls"></div>
</div>
<div class="slot" id="slot">
<span class="ticker" id="ticker">READY</span>
</div>
</div>
</div>
<div class="handlePanel">
<div class="handle" id="handle">
<div class="hub"></div>
<div class="arm" id="arm"><div class="knob"></div></div>
</div>
<div class="handleHint" id="handleHint">ハンドルを回す<br>(=ガチャ開始)</div>
</div>
</div>
<div id="resultBox" class="resultBox">
<p class="resultTitle">RESULT</p>
<p id="rankText" class="rank">未抽選</p>
<p id="amountText" class="amount">—</p>
<p id="hintText" class="hint">「ガチャを回す!」でハンドル演出→判定までワクワク✨</p>
<div class="btnRow">
<button id="rollBtn">ガチャを回す!</button>
<button id="resetBtn" class="secondary">履歴リセット</button>
</div>
<div class="chips">
<div class="chip">1等:10,000円</div>
<div class="chip">2等:5,000円</div>
<div class="chip">3等:2,000円</div>
<div class="chip">4等:500円</div>
</div>
<div class="footerNote">
※ 乱数は <code>crypto.getRandomValues</code> を使用。<br>
※ 判定前に「回転→カプセル落下→ドラムロール」っぽい間を入れてます。
</div>
</div>
</div>
</div>
</section>
<aside class="card">
<div class="hd">
<div class="badge">確率ガチャ設定</div>
<div class="badge" id="sumBadge">合計: 100%</div>
</div>
<div class="body">
<div class="rows" id="probRows"></div>
<div id="sumMsg" class="ok">OK:合計が100%です ✅</div>
<div style="margin-top:14px;">
<div class="badge">履歴</div>
</div>
<div class="history" id="history"></div>
<div class="sumRow">
<div>合計お年玉</div>
<div id="sumAmount">0円</div>
</div>
</div>
</aside>
</div>
</div>
<script>
(() => {
const prizes = [
{ key:"1", name:"1等", amount:10000, prob: 1, theme:"gold" },
{ key:"2", name:"2等", amount: 5000, prob: 6, theme:"purple"},
{ key:"3", name:"3等", amount: 2000, prob: 23, theme:"blue" },
{ key:"4", name:"4等", amount: 500, prob: 70, theme:"green" }
];
const stage = document.getElementById("stage");
const fxLayer = document.getElementById("fxLayer");
const veil = document.getElementById("veil");
const resultBox = document.getElementById("resultBox");
const rankText = document.getElementById("rankText");
const amountText = document.getElementById("amountText");
const hintText = document.getElementById("hintText");
const rollBtn = document.getElementById("rollBtn");
const resetBtn = document.getElementById("resetBtn");
const probRows = document.getElementById("probRows");
const sumBadge = document.getElementById("sumBadge");
const sumMsg = document.getElementById("sumMsg");
const historyEl = document.getElementById("history");
const sumAmountEl = document.getElementById("sumAmount");
const soundToggle = document.getElementById("soundToggle");
const handle = document.getElementById("handle");
const handleHint = document.getElementById("handleHint");
const balls = document.getElementById("balls");
const slot = document.getElementById("slot");
const ticker = document.getElementById("ticker");
let rolling = false;
let history = [];
let total = 0;
const fmtYen = (n) => n.toLocaleString("ja-JP") + "円";
const nowTime = () => new Date().toLocaleTimeString("ja-JP", {hour:"2-digit", minute:"2-digit", second:"2-digit"});
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
function rand01(){
const u = new Uint32Array(1);
crypto.getRandomValues(u);
return u[0] / 4294967296;
}
function sumProb(){ return prizes.reduce((a,p)=>a + Number(p.prob || 0), 0); }
function updateSumUI(){
const s = sumProb();
sumBadge.textContent = `合計: ${s}%`;
const ok = (s === 100);
sumMsg.className = ok ? "ok" : "warn";
sumMsg.textContent = ok ? "OK:合計が100%です ✅" : `注意:合計が100%になるように調整してね(今 ${s}%)⚠️`;
rollBtn.disabled = rolling || !ok;
}
function drawPrize(){
const r = rand01() * 100;
let acc = 0;
for(const p of prizes){
acc += Number(p.prob);
if(r < acc) return p;
}
return prizes[prizes.length - 1];
}
let audioCtx = null;
function ensureAudio(){
if(!soundToggle.checked) return null;
if(!audioCtx){
const AC = window.AudioContext || window.webkitAudioContext;
if(!AC) return null;
audioCtx = new AC();
}
if(audioCtx.state === "suspended"){
audioCtx.resume().catch(()=>{});
}
return audioCtx;
}
function beep({freq=440, dur=0.10, type="sine", gain=0.06}={}){
const ctx = ensureAudio();
if(!ctx) return;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = type;
o.frequency.value = freq;
g.gain.value = 0.0001;
o.connect(g); g.connect(ctx.destination);
const t = ctx.currentTime;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(gain, t + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.start(t);
o.stop(t + dur + 0.02);
}
async function tickSequence(msTotal=900){
const steps = Math.max(8, Math.floor(msTotal / 70));
for(let i=0;i<steps;i++){
const f = 280 + i*18 + (rand01()*18);
beep({freq:f, dur:0.03, type:"sine", gain:0.03});
await sleep(55 + rand01()*25);
}
}
function fanfareFor(prize){
if(!soundToggle.checked) return;
if(prize.key === "1"){
beep({freq:523.25, dur:0.09, type:"triangle", gain:0.07});
setTimeout(()=>beep({freq:659.25, dur:0.10, type:"triangle", gain:0.07}), 110);
setTimeout(()=>beep({freq:783.99, dur:0.14, type:"triangle", gain:0.08}), 240);
}else if(prize.key === "2"){
beep({freq:523.25, dur:0.09, type:"sine", gain:0.06});
setTimeout(()=>beep({freq:659.25, dur:0.10, type:"sine", gain:0.06}), 120);
}else if(prize.key === "3"){
beep({freq:440, dur:0.07, type:"square", gain:0.045});
setTimeout(()=>beep({freq:554.37, dur:0.08, type:"square", gain:0.045}), 90);
}else{
beep({freq:392, dur:0.06, type:"sine", gain:0.035});
}
}
function clearFX(){
fxLayer.innerHTML = "";
stage.classList.remove("shake");
resultBox.classList.remove("glow1","glow2","pop");
veil.classList.remove("on");
balls.classList.remove("spinning");
slot.classList.remove("rolling");
handle.classList.remove("turning");
}
function confetti(count=90){
const colors = ["#fde047","#f97316","#a78bfa","#60a5fa","#34d399","#f43f5e","#ffffff"];
for(let i=0;i<count;i++){
const el = document.createElement("div");
el.className = "confetti";
el.style.setProperty("--x", Math.floor(rand01()*100) + "%");
el.style.setProperty("--rot", Math.floor(rand01()*360) + "deg");
el.style.setProperty("--dur", (1.2 + rand01()*0.9).toFixed(2) + "s");
el.style.background = colors[Math.floor(rand01()*colors.length)];
el.style.width = (8 + rand01()*8) + "px";
el.style.height = (10 + rand01()*14) + "px";
el.style.opacity = (0.75 + rand01()*0.25).toFixed(2);
fxLayer.appendChild(el);
setTimeout(()=>el.remove(), 2400);
}
}
function sparkles(count=36){
for(let i=0;i<count;i++){
const s = document.createElement("div");
s.className = "spark";
s.style.setProperty("--x", (10 + rand01()*80) + "%");
s.style.setProperty("--y", (15 + rand01()*70) + "%");
fxLayer.appendChild(s);
setTimeout(()=>s.remove(), 900);
}
}
function burst(count=18){
for(let i=0;i<count;i++){
const b = document.createElement("div");
b.className = "burst";
const ang = rand01() * Math.PI * 2;
const dist = 80 + rand01()*90;
b.style.setProperty("--dx", Math.cos(ang) * dist + "px");
b.style.setProperty("--dy", Math.sin(ang) * dist + "px");
b.style.background = `rgba(255,255,255,${0.65 + rand01()*0.3})`;
fxLayer.appendChild(b);
setTimeout(()=>b.remove(), 850);
}
}
function applyTheme(prize){
const themeMap = {
gold: ["#fde047", "rgba(253,224,71,.18)"],
purple: ["#a78bfa", "rgba(167,139,250,.16)"],
blue: ["#60a5fa", "rgba(96,165,250,.15)"],
green: ["#34d399", "rgba(52,211,153,.14)"],
};
const [c, glow] = themeMap[prize.theme] || ["#fff","rgba(255,255,255,.10)"];
rankText.style.color = c;
amountText.style.color = c;
resultBox.style.boxShadow = `0 0 0 1px rgba(255,255,255,.08), 0 0 48px ${glow}`;
}
function playFX(prize){
applyTheme(prize);
if(prize.key === "1"){
resultBox.classList.add("glow1");
confetti(100);
setTimeout(()=>sparkles(24), 120);
setTimeout(()=>sparkles(24), 320);
}else if(prize.key === "2"){
resultBox.classList.add("glow2");
sparkles(44);
setTimeout(()=>sparkles(28), 260);
}else if(prize.key === "3"){
resultBox.classList.add("pop");
burst(26);
}else{
stage.classList.add("shake");
setTimeout(()=>stage.classList.remove("shake"), 650);
sparkles(10);
}
}
function buildBalls(){
balls.innerHTML = "";
const spots = [[18,18],[50,14],[62,30],[30,52],[58,58],[20,62],[40,38],[70,48]];
for(let i=0;i<spots.length;i++){
const d = document.createElement("div");
d.className = "ball";
d.style.left = spots[i][0] + "%";
d.style.top = spots[i][1] + "%";
d.style.setProperty("--dx", (Math.floor(-10 + rand01()*20)) + "px");
d.style.setProperty("--dy", (Math.floor(-10 + rand01()*20)) + "px");
d.style.background = `rgba(255,255,255,${0.65 + rand01()*0.28})`;
balls.appendChild(d);
}
}
const tickWords = ["READY","SPIN...","GACHA!","??","...","!!!","ROLL"];
function startTicker(){
slot.classList.add("rolling");
let i=0;
ticker.textContent = "SPIN...";
const id = setInterval(()=>{
ticker.textContent = tickWords[i % tickWords.length];
i++;
}, 120);
return () => { clearInterval(id); slot.classList.remove("rolling"); };
}
function buildProbRows(){
probRows.innerHTML = "";
prizes.forEach((p) => {
const row = document.createElement("div");
row.className = "row";
row.innerHTML = `
<div class="left">
<div class="label">
<span>${p.name}(${fmtYen(p.amount)})</span>
<span class="pct" id="pct-${p.key}">${p.prob}%</span>
</div>
<small>確率を調整(合計100%にしてね)</small>
</div>
<div>
<input id="rng-${p.key}" type="range" min="0" max="100" step="1" value="${p.prob}" aria-label="${p.name}の確率" />
</div>
`;
probRows.appendChild(row);
const rng = row.querySelector(`#rng-${p.key}`);
const pct = row.querySelector(`#pct-${p.key}`);
rng.addEventListener("input", () => {
p.prob = Number(rng.value);
pct.textContent = `${p.prob}%`;
updateSumUI();
});
});
updateSumUI();
}
function renderHistory(){
historyEl.innerHTML = "";
if(history.length === 0){
const empty = document.createElement("div");
empty.style.color = "var(--muted)";
empty.style.fontSize = "13px";
empty.style.padding = "10px 6px";
empty.textContent = "まだ履歴はありません。";
historyEl.appendChild(empty);
}else{
for(const h of history.slice().reverse()){
const div = document.createElement("div");
div.className = "histItem";
div.innerHTML = `
<div class="histLeft">
<div class="histRank">${h.prize.name}</div>
<div class="histTime">${h.t}</div>
</div>
<div style="font-weight:900;">${fmtYen(h.prize.amount)}</div>
`;
historyEl.appendChild(div);
}
}
sumAmountEl.textContent = fmtYen(total);
}
function resetAll(){
history = [];
total = 0;
rankText.textContent = "未抽選";
amountText.textContent = "—";
hintText.textContent = "「ガチャを回す!」でハンドル演出→判定までワクワク✨";
rankText.style.color = "";
amountText.style.color = "";
resultBox.style.boxShadow = "";
ticker.textContent = "READY";
clearFX();
renderHistory();
}
async function roll(){
if(rolling) return;
if(sumProb() !== 100) return;
rolling = true;
rollBtn.disabled = true;
resetBtn.disabled = true;
handleHint.textContent = "回転中…";
veil.classList.add("on");
clearFX();
buildBalls();
hintText.textContent = "ハンドルを回して…いくよ…!🌀";
rankText.textContent = "抽選中…";
amountText.textContent = "…";
handle.classList.add("turning");
balls.classList.add("spinning");
const stopTicker = startTicker();
for(let i=0;i<6;i++){
beep({freq: 420 + i*25, dur:0.03, type:"square", gain:0.03});
await sleep(120);
}
hintText.textContent = "カプセルが出てくる…!✨";
await tickSequence(820 + rand01()*420);
beep({freq: 180, dur:0.08, type:"sine", gain:0.04});
await sleep(220);
const prize = drawPrize();
stopTicker();
balls.classList.remove("spinning");
handle.classList.remove("turning");
rankText.textContent = prize.name;
amountText.textContent = fmtYen(prize.amount);
const msgMap = {
"1": "🎉 大当たり!!金ピカ演出いきます!!",
"2": "✨ ナイス!いいお年玉!",
"3": "🙂 ちょうど嬉しいライン!",
"4": "😆 参加賞!次いこ次!",
};
hintText.textContent = msgMap[prize.key] || "結果が出ました!";
playFX(prize);
fanfareFor(prize);
history.push({ t: nowTime(), prize });
total += prize.amount;
renderHistory();
await sleep(950);
veil.classList.remove("on");
handleHint.textContent = "もう一回回す?";
rolling = false;
resetBtn.disabled = false;
updateSumUI();
}
rollBtn.addEventListener("click", roll);
handle.addEventListener("click", roll);
resetBtn.addEventListener("click", resetAll);
document.addEventListener("pointerdown", () => ensureAudio(), { once:true });
buildProbRows();
buildBalls();
renderHistory();
updateSumUI();
})();
</script>
</body>
</html>
</details>
ブラウザによっては「初回クリックで音が解禁」されます。
このコードは最初の操作で AudioContext を起動するようにしてあるので、だいたい解決します🔊
合計が100%じゃないと回せない仕様です!
右上の「合計」を見て 100% に調整してください✅
CSSの .machine や .handlePanel の背景色を変えるだけで雰囲気が変わります。
(例:赤・金に寄せると一気に正月っぽく🎍✨)
正直、抽選だけなら一瞬で終わります。
でも、**「回してる感」+「待ち」+「当たりの演出差」**を入れると、同じ抽選でもワクワクが段違いになります🎰🔥
次は、もしやるなら👇も面白いです!
必要なら、**「10連対応版」**に改造したコードもそのまま作れます。