ทำ Spinning Reel Animation ด้วย CSS + JS แบบมือโปร
เผยแพร่เมื่อ: 11 มีนาคม 2026 | หมวดหมู่: JavaScript & CSS | โดย: GlowCode Team
📋 สิ่งที่คุณจะได้เรียนรู้ในบทความนี้
- ทำไม Slot Reel ถึงเป็น use case ที่ดีสำหรับนักพัฒนา — CSS Transform, State Machine, และ DOM Cloning
- HTML Structure — โครงสร้างที่ clean และ JS-injectable
- CSS ทั้งหมด — overflow: hidden, payline, GPU compositing, และ win animation
- JavaScript แบ่ง 5 ส่วน:
- Config & State management
- buildReel() — DOM injection + initial position
- Easing functions จริง (easeOutQuart, easeInOutCubic, easeOutBounce)
- spinReel() — physics animation loop ด้วย requestAnimationFrame
- spin() — orchestration ด้วย Promise.all + delay per reel
- Infinite Loop technique — modulo wrap ป้องกัน strip หมด
- Performance Optimization — will-change, rAF vs setInterval, composite-only properties
- Full single-file working demo — copy-paste รันได้เลย
- Summary table — สรุปเทคนิคทั้งหมดแบบ skim ง่าย
⏱️ เวลาอ่านโดยประมาณ: 15–20 นาที | ระดับ: Intermediate
ทำความเข้าใจ Slot Reel Animation พื้นฐานสำคัญก่อนเริ่มเขียนโค้ด
ก่อนที่เราจะเริ่มเขียนโค้ด เราควรทำความเข้าใจพื้นฐานของ Slot Reel Animation กันก่อน เพราะจริง ๆ แล้วมันไม่ได้เป็นเพียงแค่ วงล้อที่หมุนได้ เท่านั้น แต่เป็นตัวอย่างที่ดีของการนำเทคนิคหลายอย่างในการพัฒนาเว็บมาทำงานร่วมกัน
การสร้าง Reel ของสล็อตต้องอาศัยแนวคิดสำคัญหลายด้าน เช่น การใช้ CSS Transform เพื่อเลื่อนตำแหน่งของวงล้อให้ดูเหมือนกำลังหมุน การใช้ overflow: hidden เพื่อซ่อนส่วนที่เกินออกไป และทำให้ผู้เล่นเห็นเฉพาะสัญลักษณ์ที่อยู่ในช่องแสดงผลเท่านั้น
นอกจากนี้ยังมี JavaScript ที่ช่วยควบคุมจังหวะของการหมุน เช่น การเริ่มหมุน การเร่งความเร็ว การชะลอความเร็ว และการหยุดในตำแหน่งที่ต้องการ รวมถึงการจัดการสถานะของระบบ เช่น จาก หยุด → กำลังหมุน → ค่อย ๆ หยุด → หยุดสนิท มันเป็นตัวอย่างที่ดีมากของ:
- CSS Transform + Overflow Hidden ที่ทำงานร่วมกัน
- JavaScript Timing & Easing Functions (Physics-based animation)
- State Machine อย่างง่าย (Idle → Spinning → Decelerating → Stopped)
- DOM Cloning เพื่อสร้าง Infinite Loop illusion
เว็บไซต์สล็อตออนไลน์อย่าง [ชื่อเว็บคาสิโน] ที่มีเกมนับร้อยใช้เทคนิคเหล่านี้เป็นพื้นฐาน แม้จะมี WebGL หรือ Canvas เข้ามาช่วยในระดับ Production แต่หลักการ Animation ยังคงเหมือนเดิม
โครงสร้างพื้นฐานของ Slot Reel Animation ที่เรากำลังจะสร้าง
แต่ละ Reel จะเป็น <div> ที่มี Symbols อยู่ข้างใน โดย CSS จะซ่อนส่วนที่เกินออกไป และ JavaScript จะควบคุม translateY() เพื่อสร้างภาพว่า Reel กำลังหมุน
ส่วนที่ 1: HTML Structure
ก่อนที่เราจะทำให้สล็อต หมุนได้ ด้วย CSS และ JavaScript เราต้องเริ่มจากการสร้าง โครงสร้างของหน้าเว็บก่อน ซึ่งก็คือ HTML นั่นเอง โดย HTML จะทำหน้าที่เหมือน โครงกระดูกของระบบสล็อต กำหนดว่ามีส่วนไหนบ้าง เช่น ช่องแสดงผล วงล้อ และสัญลักษณ์
ลองนึกภาพง่าย ๆ ว่าเกมสล็อตหนึ่งเครื่องจะมี ช่องแสดงผลตรงกลาง และมี Reel หรือวงล้อ อยู่ด้านใน ซึ่งแต่ละวงล้อก็จะมีสัญลักษณ์ต่าง ๆ เรียงต่อกัน
ซึ่งจะเริ่มต้นด้วยโครงสร้าง
HTML ที่ Clean และ Semantic:
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slot Machine Demo</title>
<link rel="stylesheet" href="slot.css">
</head>
<body>
<div class="slot-machine">
<div class="machine-frame">
<!-- Display Window -->
<div class="reels-container">
<div class="reel" id="reel-0">
<div class="reel-strip">
<!-- Symbols จะถูก inject ด้วย JS -->
</div>
</div>
<div class="reel" id="reel-1">
<div class="reel-strip"></div>
</div>
<div class="reel" id="reel-2">
<div class="reel-strip"></div>
</div>
</div>
<!-- Payline Indicator -->
<div class="payline"></div>
</div>
<!-- Controls -->
<div class="controls">
<button class="spin-btn" id="spinBtn">SPIN</button>
</div>
<!-- Result Message -->
<div class="result" id="result"></div>
</div>
<script src="slot.js"></script>
</body>
</html>
หมายเหตุ: เราจงใจไม่ใส่ Symbols ใน HTML โดยตรง เพราะ JavaScript จะ generate และ inject เข้าไป ทำให้ง่ายต่อการ configure และ randomize ในภายหลัง
ส่วนที่ 2: CSS — รากฐานของ Reel
ในส่วนนี้คือจุดที่ทำให้สล็อตเริ่ม มีชีวิต ขึ้นมา เพราะ CSS เป็นตัวกำหนดโครงสร้างของ Reel และสร้างภาพลวงตาว่าวงล้อกำลังหมุนอยู่ แม้จริง ๆ แล้วมันเป็นเพียงการเลื่อนตำแหน่งขององค์ประกอบบนหน้าเว็บเท่านั้น
Reel คือ หน้าต่าง ที่มองเข้าไปใน Strip ที่ยาวกว่า
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #1a1a2e;
font-family: 'Segoe UI', sans-serif;
}
/* === MACHINE FRAME === */
.slot-machine {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.machine-frame {
position: relative;
background: linear-gradient(145deg, #2d1b69, #11052c);
border: 4px solid #ffd700;
border-radius: 16px;
padding: 24px;
box-shadow:
0 0 30px rgba(255, 215, 0, 0.3),
inset 0 0 20px rgba(0,0,0,0.5);
}
/* === REELS CONTAINER === */
.reels-container {
display: flex;
gap: 8px;
background: #000;
border-radius: 8px;
overflow: hidden; /* สำคัญมาก! */
border: 2px solid #333;
}
/* === SINGLE REEL (the "window") === */
.reel {
width: 100px;
height: 300px; /* แสดง 3 symbols พอดี (แต่ละ symbol = 100px) */
overflow: hidden; /* ซ่อน symbols ที่ไม่อยู่ใน frame */
position: relative;
background: #111;
}
/* === REEL STRIP (แถบที่หมุน) === */
.reel-strip {
display: flex;
flex-direction: column;
will-change: transform; /* บอก browser ล่วงหน้าว่าจะมี transform */
transform: translateY(0);
/* ไม่ใส่ CSS transition ที่นี่ เพราะเราจะควบคุมด้วย JS แทน */
}
/* === SYMBOL CELL === */
.symbol {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
flex-shrink: 0;
user-select: none;
border-bottom: 1px solid #222;
transition: none; /* สำคัญ: ปิด transition บน symbol */
}
/* === PAYLINE (เส้นกลาง) === */
.payline {
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 102px; /* ความสูงเท่า symbol 1 ช่อง + เล็กน้อย */
border-top: 2px solid rgba(255, 215, 0, 0.6);
border-bottom: 2px solid rgba(255, 215, 0, 0.6);
pointer-events: none;
background: rgba(255,215,0,0.04);
}
/* === SPIN BUTTON === */
.spin-btn {
padding: 14px 48px;
font-size: 18px;
font-weight: bold;
letter-spacing: 3px;
background: linear-gradient(145deg, #ff6b35, #f7c59f);
color: #1a1a2e;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.4);
transition: transform 0.1s, box-shadow 0.1s;
}
.spin-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.6);
}
.spin-btn:active {
transform: translateY(1px);
}
.spin-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* === RESULT MESSAGE === */
.result {
font-size: 22px;
font-weight: bold;
color: #ffd700;
min-height: 32px;
text-align: center;
text-shadow: 0 0 10px rgba(255,215,0,0.5);
letter-spacing: 1px;
}
/* === WIN ANIMATION === */
@keyframes winPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
.symbol.winning {
animation: winPulse 0.4s ease-in-out infinite;
background: rgba(255, 215, 0, 0.2);
border-radius: 8px;
}
ส่วนที่ 3: JavaScript — หัวใจของ Animation
หลังจากที่เราเตรียมโครงสร้างด้วย HTML และวางพื้นฐานการแสดงผลด้วย CSS แล้ว ขั้นตอนต่อไปคือการทำให้สล็อต เคลื่อนไหวได้จริง ซึ่งหน้าที่นี้เป็นของ JavaScript
ถ้าเปรียบเทียบง่าย ๆ
- HTML คือโครงสร้างของเครื่องสล็อต
- CSS คือหน้าตาและพื้นที่แสดงผล
- JavaScript คือระบบที่สั่งให้วงล้อเริ่มหมุน หยุดหมุน และควบคุมจังหวะของ Animation
นี่คือส่วนที่ซับซ้อนที่สุด เราจะแบ่งออกเป็นหลายส่วน:
3.1 Configuration & State
Configuration & State คือการกำหนดค่าพื้นฐานและสถานะของระบบในการทำงาน
Configuration หมายถึงค่าที่ตั้งไว้ล่วงหน้า เช่น จำนวน Reel ความเร็วการหมุน หรือจำนวนสัญลักษณ์ในเกม
ส่วน State คือสถานะปัจจุบันของระบบ เช่น หยุดอยู่ กำลังหมุน หรือกำลังชะลอความเร็ว เพื่อให้ระบบรู้ว่าควรทำงานขั้นตอนถัดไปอย่างไร
// ===========================
// CONFIGURATION
// ===========================
const SYMBOLS = ['🍒', '🍋', '🍊', '🍇', '💎', '⭐', '7️⃣', '🔔'];
const SYMBOL_HEIGHT = 100; // px — ต้องตรงกับ CSS
const VISIBLE_SYMBOLS = 3; // จำนวน symbol ที่เห็นใน reel
const NUM_REELS = 3;
// Physics settings
const MIN_SPIN_DURATION = 1500; // ms
const MAX_SPIN_DURATION = 3500; // ms
const REEL_DELAY = 300; // ms ระหว่าง reel แต่ละอัน
// Symbols per reel strip (เยอะพอสำหรับ seamless loop)
const STRIP_SYMBOL_COUNT = 30;
// ===========================
// STATE
// ===========================
let isSpinning = false;
const reelStates = []; // เก็บ state ของแต่ละ reel
3.2 สร้าง Reel Strip
การสร้าง Reel Strip คือการจัดเรียงสัญลักษณ์ของเกมสล็อต เช่น 🍒 💎 7️⃣ ⭐ ให้อยู่ต่อกันเป็นแนวยาวภายในหนึ่ง Reel โดยสัญลักษณ์เหล่านี้จะถูกวางซ้อนกันในแนวตั้ง เพื่อให้สามารถเลื่อนขึ้นลงได้ เมื่อ Animation ทำงาน ผู้เล่นจะเห็นเหมือนวงล้อกำลังหมุนและแสดงผลลัพธ์ของเกม.
// BUILD REELS
// ===========================
function buildReel(reelEl, index) {
const strip = reelEl.querySelector('.reel-strip');
// สร้าง symbol list แบบ random สำหรับ strip นี้
const symbolList = [];
for (let i = 0; i < STRIP_SYMBOL_COUNT; i++) {
symbolList.push(SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]);
}
// Render symbols เข้า DOM
strip.innerHTML = '';
symbolList.forEach(sym => {
const el = document.createElement('div');
el.className = 'symbol';
el.textContent = sym;
strip.appendChild(el);
});
// คำนวณ initial position ให้ reel เริ่มที่กลางหน้าจอ
// (ให้ symbol แรกอยู่ตรง payline)
const initialOffset = -SYMBOL_HEIGHT; // offset ไป 1 row เพื่อให้ symbol ที่ 2 อยู่กลาง
strip.style.transform = `translateY(${initialOffset}px)`;
// เก็บ state
reelStates[index] = {
strip,
symbolList,
currentOffset: initialOffset,
animFrameId: null,
};
}
function initAllReels() {
for (let i = 0; i < NUM_REELS; i++) {
const reelEl = document.getElementById(`reel-${i}`);
buildReel(reelEl, i);
}
}
3.3 Easing Function
Easing Function คือเทคนิคที่ใช้ควบคุม จังหวะความเร็ว ของ Animation ให้ดูเป็นธรรมชาติ ไม่ได้เคลื่อนที่ด้วยความเร็วคงที่ตลอดเวลา
ถ้า Animation เคลื่อนที่แบบความเร็วเท่ากันตั้งแต่ต้นจนจบ มันจะดูแข็งและไม่สมจริง แต่ Easing Function จะช่วยให้การเคลื่อนที่มีจังหวะเหมือนของจริง เช่น เริ่มช้า → เร็วขึ้น → ค่อย ๆ ช้าลงก่อนหยุด
ตัวอย่างเช่น เวลาวงล้อสล็อตหมุน ตอนเริ่มจะเร่งความเร็วขึ้นอย่างรวดเร็ว จากนั้นเมื่อใกล้หยุดก็จะค่อย ๆ ช้าลงจนหยุดพอดี ซึ่งทั้งหมดนี้ถูกควบคุมด้วย Easing Function
ดังนั้น Easing Function จึงช่วยให้ Animation ดู ลื่นไหล นุ่มนวล และสมจริงมากขึ้น แทนที่จะเป็นการเคลื่อนที่แบบแข็ง ๆ ธรรมดา.
// EASING FUNCTIONS
// ===========================
/**
* easeOutQuart — ชะลอแบบ exponential
* t = 0 (เริ่ม) → 1 (จบ)
* return = 0 → 1 (ความก้าวหน้า)
*/
function easeOutQuart(t) {
return 1 - Math.pow(1 - t, 4);
}
/**
* easeInOutCubic — เริ่มช้า → เร็ว → ช้า
* ใช้สำหรับ spin phase ที่ต้องการ acceleration ตอนเริ่ม
*/
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}
/**
* easeOutBounce — กระเด้งเล็กน้อยตอนหยุด (optional ใช้กับ premium feel)
*/
function easeOutBounce(t) {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
3.4 Spin Animation Logic (หัวใจหลัก)
Spin Animation Logic คือระบบที่ควบคุมการหมุนของ Reel ในเกมสล็อต โดยกำหนดลำดับการทำงาน เช่น เริ่มหมุน เร่งความเร็ว หมุนต่อเนื่อง แล้วค่อย ๆ ชะลอจนหยุดในตำแหน่งที่กำหนด JavaScript จะคำนวณเวลาและตำแหน่งของ Reel เพื่อให้การหมุนดูสมจริง ลื่นไหล และหยุดตรงสัญลักษณ์ที่ต้องการพอดี.
// SPIN A SINGLE REEL
// ===========================
/**
* spinReel — animate reel หนึ่งอันให้หมุนและหยุด
* @param {number} reelIndex
* @param {number} duration - ms ที่จะใช้หมุน
* @param {number} targetSymbolIndex - index ของ symbol ที่ต้องการให้หยุด
* @returns {Promise} - resolve เมื่อ reel หยุด
*/
function spinReel(reelIndex, duration, targetSymbolIndex) {
return new Promise(resolve => {
const state = reelStates[reelIndex];
const { strip, symbolList } = state;
// คำนวณระยะทางที่ต้องหมุน
// เราต้องการหมุนอย่างน้อย N รอบ แล้วหยุดที่ targetSymbolIndex
const fullStrip = symbolList.length * SYMBOL_HEIGHT;
// คำนวณ final position:
// targetOffset = position ที่ทำให้ targetSymbol อยู่ตรงกลาง payline
// payline อยู่ที่ row ที่ 2 (index 1) ของ visible area
const targetOffset = -(targetSymbolIndex * SYMBOL_HEIGHT) + SYMBOL_HEIGHT;
// หมุนอย่างน้อย 5 รอบเต็มก่อนถึง target
const minRounds = 5;
const spinDistance = fullStrip * minRounds + Math.abs(targetOffset - state.currentOffset);
const startOffset = state.currentOffset;
const endOffset = startOffset - spinDistance; // หมุนลง (translateY ลดลง)
// อย่าให้ค่า offset ติดลบมากเกินไป — wrap กลับมาด้วย modulo
const adjustedEnd = endOffset;
let startTime = null;
function animate(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
// ใช้ easing: ช่วงแรก linear, ช่วงท้าย ease out
let easedProgress;
if (progress < 0.7) {
// 70% แรก: หมุนเร็วแบบ linear (รู้สึกว่าหมุนเต็มที่)
easedProgress = progress / 0.7 * 0.7;
} else {
// 30% หลัง: ชะลอด้วย easeOutQuart
const decelProgress = (progress - 0.7) / 0.3;
easedProgress = 0.7 + easeOutQuart(decelProgress) * 0.3;
}
const currentOffset = startOffset + (adjustedEnd - startOffset) * easedProgress;
// Wrap offset เพื่อป้องกัน DOM ถูกดึงไกลเกิน
const wrappedOffset = ((currentOffset % fullStrip) - fullStrip) % fullStrip;
strip.style.transform = `translateY(${wrappedOffset}px)`;
state.currentOffset = wrappedOffset;
if (progress < 1) {
state.animFrameId = requestAnimationFrame(animate);
} else {
// Snap to nearest symbol เพื่อให้ align พอดี
snapToSymbol(reelIndex, targetSymbolIndex);
resolve(targetSymbolIndex);
}
}
state.animFrameId = requestAnimationFrame(animate);
});
}
/**
* snapToSymbol — snap reel ให้ align พอดีกับ symbol
*/
function snapToSymbol(reelIndex, symbolIndex) {
const state = reelStates[reelIndex];
const { strip, symbolList } = state;
const fullStrip = symbolList.length * SYMBOL_HEIGHT;
// คำนวณ exact offset
let snapOffset = -(symbolIndex * SYMBOL_HEIGHT) + SYMBOL_HEIGHT;
// Wrap เข้า range ที่ถูกต้อง
snapOffset = ((snapOffset % fullStrip) - fullStrip) % fullStrip;
if (snapOffset > 0) snapOffset -= fullStrip;
strip.style.transform = `translateY(${snapOffset}px)`;
state.currentOffset = snapOffset;
}
3.5 ควบคุมทุก Reels พร้อมกัน
การควบคุมทุก Reels พร้อมกัน คือการสั่งให้วงล้อสล็อตหลายวงทำงานในเวลาเดียวกัน โดย JavaScript จะควบคุมการเริ่มหมุน ความเร็ว และเวลาหยุดของแต่ละ Reel อาจเริ่มพร้อมกันแต่หยุดทีละวง เพื่อให้เกมดูสมจริงและสร้างความตื่นเต้นเหมือนเกมสล็อตจริง.
// SPIN ALL REELS
// ===========================
/**
* getTargetSymbols — เลือก symbols ที่ต้องการให้หยุด
* ในระบบจริง ค่าตรงนี้จะมาจาก Server (RNG backend)
* ใน demo นี้เราสุ่มฝั่ง client
*/
function getTargetSymbols() {
// สุ่มปกติ
return Array.from({ length: NUM_REELS }, () =>
Math.floor(Math.random() * STRIP_SYMBOL_COUNT)
);
}
/**
* checkWin — ตรวจสอบว่าชนะหรือไม่
*/
function checkWin(symbolIndices) {
const symbols = symbolIndices.map(
(idx, reel) => reelStates[reel].symbolList[idx]
);
console.log('Result symbols:', symbols);
if (symbols[0] === symbols[1] && symbols[1] === symbols[2]) {
return { win: true, type: 'jackpot', symbols };
}
if (symbols[0] === symbols[1] || symbols[1] === symbols[2]) {
return { win: true, type: 'small', symbols };
}
return { win: false, symbols };
}
/**
* spin — main function เรียกเมื่อกด SPIN
*/
async function spin() {
if (isSpinning) return;
isSpinning = true;
const spinBtn = document.getElementById('spinBtn');
const resultEl = document.getElementById('result');
spinBtn.disabled = true;
resultEl.textContent = '';
// ล้าง winning state
document.querySelectorAll('.symbol.winning').forEach(el => {
el.classList.remove('winning');
});
// เลือก target symbols
const targets = getTargetSymbols();
// สร้าง promises สำหรับแต่ละ reel พร้อม delay
const spinPromises = targets.map((target, i) => {
const duration = MIN_SPIN_DURATION + (i * REEL_DELAY) +
Math.random() * (MAX_SPIN_DURATION - MIN_SPIN_DURATION);
return new Promise(resolve => {
setTimeout(() => {
spinReel(i, duration, target).then(resolve);
}, i * 200); // เริ่ม reel ไม่พร้อมกันเล็กน้อย
});
});
// รอให้ทุก reel หยุด
await Promise.all(spinPromises);
// ตรวจสอบผล
const result = checkWin(targets);
displayResult(result);
isSpinning = false;
spinBtn.disabled = false;
}
/**
* displayResult — แสดงผลลัพธ์
*/
function displayResult(result) {
const resultEl = document.getElementById('result');
if (result.win && result.type === 'jackpot') {
resultEl.textContent = '🎉 JACKPOT! ชนะใหญ่! 🎉';
resultEl.style.color = '#ffd700';
highlightWinningSymbols();
} else if (result.win) {
resultEl.textContent = '✨ ได้รางวัลเล็กน้อย!';
resultEl.style.color = '#90ee90';
highlightWinningSymbols();
} else {
resultEl.textContent = 'ไม่ได้รางวัล ลองอีกครั้ง!';
resultEl.style.color = '#aaa';
}
}
/**
* highlightWinningSymbols — highlight symbols ที่ชนะ
*/
function highlightWinningSymbols() {
// หา symbols ที่อยู่ใน payline (row กลาง = index 1 ของ visible area)
for (let i = 0; i < NUM_REELS; i++) {
const reelEl = document.getElementById(`reel-${i}`);
const symbols = reelEl.querySelectorAll('.symbol');
// หา visible symbols
const state = reelStates[i];
const offset = state.currentOffset;
const visibleStartIndex = Math.round(-offset / SYMBOL_HEIGHT) - 1;
const middleIndex = visibleStartIndex + 1; // row กลาง
if (symbols[middleIndex]) {
symbols[middleIndex].classList.add('winning');
}
}
}
// ===========================
// INIT
// ===========================
document.addEventListener('DOMContentLoaded', () => {
initAllReels();
document.getElementById('spinBtn').addEventListener('click', spin);
});
ส่วนที่ 4: Infinite Loop — ป้องกัน Strip หมด
Infinite Loop คือเทคนิคที่ทำให้ Reel หมุนได้ต่อเนื่องโดยไม่เห็นจุดสิ้นสุด หากสัญลักษณ์ใน Reel เลื่อนจนใกล้หมด ระบบจะนำสัญลักษณ์เดิมกลับมาต่อท้ายอีกครั้ง ทำให้ดูเหมือนวงล้อหมุนไม่รู้จบ ผู้เล่นจึงไม่เห็นจุดเริ่มต้นหรือจุดจบของ Reel เลย. ปัญหาใหญ่ของ Reel Animation คือถ้าหมุนนานพอ Strip จะหมด เราแก้ด้วย Virtual Infinite Strip:
* เพิ่มในฟังก์ชัน animate() ก่อนที่จะ set transform
* Virtual scroll: เมื่อ strip ถึง end ให้ jump กลับ top โดยไม่ให้เห็น
*/
function getVirtualOffset(rawOffset, fullStrip) {
// Normalize ให้อยู่ใน range [-fullStrip, 0]
let normalized = rawOffset % fullStrip;
if (normalized > 0) normalized -= fullStrip;
return normalized;
}
// ใน animate loop:
const virtualOffset = getVirtualOffset(currentOffset, fullStrip);
strip.style.transform = `translateY(${virtualOffset}px)`;
เทคนิคนี้ทำให้ไม่ว่าจะหมุนนานแค่ไหน DOM ก็จะไม่ถูกดึงออกไปไกล
ส่วนที่ 5: Performance Optimization
Performance Optimization คือการปรับปรุงโค้ดและการทำงานของ Animation ให้ทำงานได้เร็วและลื่นไหลมากขึ้น เช่น ใช้ transform แทนการเปลี่ยนตำแหน่งแบบอื่น ลดการคำนวณที่ไม่จำเป็น และจัดการ DOM อย่างเหมาะสม เพื่อให้ Reel หมุนได้ลื่น ไม่กระตุก และรองรับการทำงานได้ดีในทุกอุปกรณ์.
ใช้ will-change อย่างถูกต้อง
.reel-strip {
will-change: transform;
}
/* ❌ ผิด: ใส่ will-change ทุก element เพราะจะกิน memory */
.symbol {
will-change: transform; /* ไม่จำเป็น */
}
ใช้ requestAnimationFrame ไม่ใช่ setInterval
setInterval(() => {
strip.style.transform = `translateY(${offset}px)`;
offset -= speed;
}, 16);
// ✅ ดี: requestAnimationFrame sync กับ 60fps/120fps โดยอัตโนมัติ
function animate(timestamp) {
strip.style.transform = `translateY(${offset}px)`;
offset -= speed;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Composite-only Properties
strip.style.transform = `translateY(${offset}px)`;
// ❌ หลีกเลี่ยง: ทำให้เกิด Layout Recalculation ทุก frame
strip.style.top = `${offset}px`;
strip.style.marginTop = `${offset}px`;
ส่วนที่ 6: Full Working Demo (รวมทุกส่วน)
Full Working Demo คือการนำทุกส่วนที่พัฒนามารวมกันให้ระบบสล็อตทำงานได้จริง ตั้งแต่โครงสร้าง HTML การจัดรูปแบบด้วย CSS และการควบคุม Animation ด้วย JavaScript เมื่อทำงานร่วมกันแล้ว ผู้ใช้จะเห็น Reel หมุน หยุด และแสดงสัญลักษณ์ได้เหมือนเกมสล็อตจริง เป็นตัวอย่างให้เข้าใจการทำงานของระบบทั้งหมด. เพื่อความสะดวก นี่คือ complete single-file version ที่รันได้ทันที:
<html lang="th">
<head>
<meta charset="UTF-8">
<title>Slot Reel Demo — GlowCode.com</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #1a1a2e;
font-family: 'Segoe UI', sans-serif;
}
.slot-machine {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.machine-frame {
position: relative;
background: linear-gradient(145deg, #2d1b69, #11052c);
border: 4px solid #ffd700;
border-radius: 16px;
padding: 24px;
box-shadow: 0 0 30px rgba(255,215,0,0.3), inset 0 0 20px rgba(0,0,0,0.5);
}
.reels-container {
display: flex;
gap: 8px;
background: #000;
border-radius: 8px;
overflow: hidden;
border: 2px solid #333;
}
.reel {
width: 100px;
height: 300px;
overflow: hidden;
position: relative;
background: #111;
}
.reel-strip {
display: flex;
flex-direction: column;
will-change: transform;
}
.symbol {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
flex-shrink: 0;
user-select: none;
border-bottom: 1px solid #222;
}
.payline {
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 102px;
border-top: 2px solid rgba(255,215,0,0.6);
border-bottom: 2px solid rgba(255,215,0,0.6);
pointer-events: none;
background: rgba(255,215,0,0.04);
}
.spin-btn {
padding: 14px 48px;
font-size: 18px;
font-weight: bold;
letter-spacing: 3px;
background: linear-gradient(145deg, #ff6b35, #f7c59f);
color: #1a1a2e;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(255,107,53,0.4);
transition: transform 0.1s;
}
.spin-btn:hover {
transform: translateY(-2px);
}
.spin-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.result {
font-size: 22px;
font-weight: bold;
color: #ffd700;
min-height: 32px;
text-align: center;
}
@keyframes winPulse {
0%,100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
.symbol.winning {
animation: winPulse 0.4s ease-in-out infinite;
background: rgba(255,215,0,0.2);
border-radius: 8px;
}
</style>
</head>
<body>
<div class="slot-machine">
<div class="machine-frame">
<div class="reels-container">
<div class="reel" id="reel-0"><div class="reel-strip"></div></div>
<div class="reel" id="reel-1"><div class="reel-strip"></div></div>
<div class="reel" id="reel-2"><div class="reel-strip"></div></div>
</div>
<div class="payline"></div>
</div>
<button class="spin-btn" id="spinBtn">SPIN</button>
<div class="result" id="result"></div>
</div>
<script>
const SYMBOLS = ['🍒','🍋','🍊','🍇','💎','⭐','7️⃣','🔔'];
const SYMBOL_HEIGHT = 100;
const NUM_REELS = 3;
const STRIP_COUNT = 30;
const MIN_DUR = 1500, MAX_DUR = 3500, REEL_DELAY = 300;
let isSpinning = false;
const reelStates = [];
function easeOutQuart(t) { return 1 - Math.pow(1 - t, 4); }
function buildReel(reelEl, idx) {
const strip = reelEl.querySelector('.reel-strip');
const syms = Array.from({length: STRIP_COUNT}, () => SYMBOLS[Math.floor(Math.random()*SYMBOLS.length)]);
strip.innerHTML = '';
syms.forEach(s => {
const el = document.createElement('div');
el.className = 'symbol';
el.textContent = s;
strip.appendChild(el);
});
const initOffset = -SYMBOL_HEIGHT;
strip.style.transform = translateY(${initOffset}px);
reelStates[idx] = { strip, symbolList: syms, currentOffset: initOffset };
}
function snapToSymbol(idx, symIdx) {
const { strip, symbolList } = reelStates[idx];
const fullStrip = symbolList.length * SYMBOL_HEIGHT;
let snap = -(symIdx * SYMBOL_HEIGHT) + SYMBOL_HEIGHT;
snap = ((snap % fullStrip) - fullStrip) % fullStrip;
if (snap > 0) snap -= fullStrip;
strip.style.transform = translateY(${snap}px);
reelStates[idx].currentOffset = snap;
}
function spinReel(idx, duration, targetIdx) {
return new Promise(resolve => {
const state = reelStates[idx];
const { strip, symbolList } = state;
const fullStrip = symbolList.length * SYMBOL_HEIGHT;
const minRounds = 5;
const spinDist = fullStrip * minRounds + fullStrip;
const startOffset = state.currentOffset;
const endOffset = startOffset - spinDist;
let startTime = null;
function animate(ts) {
if (!startTime) startTime = ts;
const progress = Math.min((ts - startTime) / duration, 1);
let eased;
if (progress < 0.7) { eased = progress; }
else { const d = (progress - 0.7) / 0.3; eased = 0.7 + easeOutQuart(d) * 0.3; }
const raw = startOffset + (endOffset - startOffset) * eased;
let wrapped = raw % fullStrip;
if (wrapped > 0) wrapped -= fullStrip;
strip.style.transform = translateY(${wrapped}px);
state.currentOffset = wrapped;
if (progress < 1) { requestAnimationFrame(animate); }
else { snapToSymbol(idx, targetIdx); resolve(targetIdx); }
}
requestAnimationFrame(animate);
});
}
function checkWin(targets) {
const syms = targets.map((t, i) => reelStates[i].symbolList[t]);
if (syms[0] === syms[1] && syms[1] === syms[2]) return { win: true, type: 'jackpot' };
if (syms[0] === syms[1] || syms[1] === syms[2]) return { win: true, type: 'small' };
return { win: false };
}
async function spin() {
if (isSpinning) return;
isSpinning = true;
const btn = document.getElementById('spinBtn');
const resultEl = document.getElementById('result');
btn.disabled = true;
resultEl.textContent = '';
document.querySelectorAll('.symbol.winning').forEach(e => e.classList.remove('winning'));
const targets = Array.from({length: NUM_REELS}, () => Math.floor(Math.random() * STRIP_COUNT));
const promises = targets.map((t, i) => new Promise(r => {
const dur = MIN_DUR + i * REEL_DELAY + Math.random() * (MAX_DUR - MIN_DUR);
setTimeout(() => spinReel(i, dur, t).then(r), i * 200);
}));
await Promise.all(promises);
const result = checkWin(targets);
if (result.win && result.type === 'jackpot') {
resultEl.style.color = '#ffd700';
resultEl.textContent = '🎉 JACKPOT! ชนะใหญ่! 🎉';
}
else if (result.win) {
resultEl.style.color = '#90ee90';
resultEl.textContent = '✨ ได้รางวัลเล็กน้อย!';
}
else {
resultEl.style.color = '#aaa';
resultEl.textContent = 'ไม่ได้รางวัล ลองอีกครั้ง!';
}
isSpinning = false;
btn.disabled = false;
}
document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < NUM_REELS; i++) buildReel(document.getElementById(reel-${i}), i);
document.getElementById('spinBtn').addEventListener('click', spin);
});
</script>
</body>
</html>
สรุปสิ่งที่เราเรียนรู้
ในบทความนี้เราได้เรียนรู้พื้นฐานของการสร้าง Slot Reel Animation ตั้งแต่การวางโครงสร้างด้วย HTML การกำหนดรูปแบบและพื้นที่แสดงผลด้วย CSS ไปจนถึงการควบคุมการหมุนของ Reel ด้วย JavaScript นอกจากนี้ยังได้เข้าใจแนวคิดสำคัญ เช่น การสร้าง Reel Strip, การควบคุม Animation, การทำ Infinite Loop และการปรับปรุง Performance เพื่อให้ระบบทำงานได้ลื่นไหลและสมจริงเหมือนเกมสล็อตบนเว็บไซต์จริง.
| เทคนิค | วัตถุประสงค์ |
| overflow: hidden บน .reel | ซ่อน symbols ที่ไม่อยู่ใน frame |
| translateY() แทน top | Performance: ใช้ GPU Compositing |
| requestAnimationFrame | Sync กับ refresh rate, ไม่กระตุก |
| easeOutQuart | ชะลอสมจริง ไม่หยุดดื้อๆ |
| will-change: transform | บอก browser เตรียม layer ล่วงหน้า |
| modulo wrap | ป้องกัน Strip หมด / Infinite loop |
| Promise.all | รอทุก reel หยุดพร้อมกันก่อนตรวจผล |
ไปต่อ: ลองดูของจริง
ถ้าอยากเห็นว่า Production Slot ที่มี WebGL, Particle Effects, และ Sound Design ทำงานยังไง ลองดูได้ที่ [ชื่อเว็บคาสิโนของคุณ] — เป็นตัวอย่างที่ดีมากสำหรับ UX และ Animation Patterns ที่นักพัฒนาสามารถนำมาเป็น reference ได้
บทความโดย GlowCode.com — แหล่งเรียนรู้การเขียนโปรแกรมภาษาไทย