Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created June 8, 2026 12:17
Show Gist options
  • Select an option

  • Save EncodeTheCode/1b9829db87dcc9882a4829329e7c1ad7 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/1b9829db87dcc9882a4829329e7c1ad7 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MGS1 Weapon Conveyor UI Recreation</title>
<style>
:root {
--bg: #ffffff;
--slot-w: 230px;
--slot-h: 84px;
--shadow: 0 10px 28px rgba(0,0,0,.22);
}
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: var(--bg);
font-family: Arial, Helvetica, sans-serif;
}
.hint {
position: fixed;
left: 20px;
top: 18px;
color: #333;
font-size: 14px;
line-height: 1.45;
opacity: 0.72;
user-select: none;
z-index: 20;
}
.stage {
position: relative;
width: 100vw;
height: 100vh;
perspective: 900px;
}
.weapon {
position: absolute;
width: var(--slot-w);
height: var(--slot-h);
transform-origin: center center;
will-change: transform, opacity, filter;
}
.weapon .card {
position: absolute;
inset: 0;
background: linear-gradient(180deg, #080808 0%, #020202 100%);
box-shadow: var(--shadow);
overflow: hidden;
border-radius: 0;
transition: filter 180ms ease, box-shadow 180ms ease;
}
.weapon .card::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 65% 35%, rgba(62, 116, 122, 0.20), transparent 42%),
linear-gradient(90deg, rgba(255,255,255,.04), rgba(255,255,255,0));
mix-blend-mode: screen;
pointer-events: none;
}
.ammo {
position: absolute;
left: 17px;
top: 28px;
font-size: 25px;
font-weight: 700;
letter-spacing: 1px;
color: #8e8e8e;
text-shadow: 0 1px 0 rgba(0,0,0,.45);
}
.gun {
position: absolute;
right: 13px;
top: 7px;
font-size: 42px;
filter: saturate(1.15);
opacity: 0.95;
transform: translateZ(0);
text-shadow: 0 0 1px rgba(255,255,255,.18);
}
.bars {
position: absolute;
right: 11px;
bottom: 10px;
display: flex;
gap: 3px;
align-items: flex-end;
}
.bar {
width: 10px;
background: linear-gradient(180deg, #ffc53b 0%, #e28f00 100%);
box-shadow: 0 0 0 1px rgba(0,0,0,.38) inset;
}
.bar.small { height: 16px; background: linear-gradient(180deg, #223758 0%, #18263b 100%); }
.bar.med { height: 23px; }
.weapon.selected .card {
outline: 1px solid rgba(255, 166, 0, .25);
box-shadow:
0 0 0 1px rgba(255, 170, 0, .22) inset,
0 0 18px rgba(255, 160, 25, .18),
var(--shadow);
animation: pulse 250ms ease-in-out infinite alternate;
}
@keyframes pulse {
from { filter: brightness(1.00); }
to { filter: brightness(1.16); }
}
@media (max-width: 760px) {
:root { --slot-w: 185px; --slot-h: 68px; }
.ammo { font-size: 22px; top: 22px; }
.gun { font-size: 34px; top: 8px; }
.bar { width: 8px; }
}
</style>
</head>
<body>
<div class="hint">
Hold A = left / previous<br>
Hold D = right / next<br>
Release to stop immediately<br>
Enter = flash selected item for 3 seconds
</div>
<div class="stage" id="stage"></div>
<script>
const stage = document.getElementById('stage');
const settings = {
moveDuration: 411,
repeatDelay: 400,
stagger: 80,
leftBoost: 38,
};
const slots = [
{ x: 68, y: 480, z: 10 },
{ x: 315, y: 480, z: 11 },
{ x: 560, y: 480, z: 12 },
{ x: 560, y: 360, z: 14 },
{ x: 560, y: 240, z: 15 },
];
const items = [
{ name: 'SOCOM', ammo: '14/120', bullets: 14 },
{ name: 'FA-MAS', ammo: '30/90', bullets: 30 },
{ name: 'NIKITA', ammo: '6/8', bullets: 6 },
{ name: 'RATIONS', ammo: '9/9', bullets: 9 },
{ name: 'MINE', ammo: '5/5', bullets: 5 },
];
const els = items.map((item) => {
const el = document.createElement('div');
el.className = 'weapon';
el.innerHTML = `
<div class="card">
<div class="ammo">${item.ammo}</div>
<div class="gun">▣</div>
<div class="bars">
<div class="bar small"></div>
${Array.from({ length: item.bullets }, (_, n) => `<div class="bar ${n < 1 ? 'small' : 'med'}"></div>`).join('')}
</div>
</div>
`;
stage.appendChild(el);
return el;
});
// order[slotIndex] = itemIndex.
// Queue position is always preserved; items never lap each other.
let order = [0, 1, 2, 3, 4];
let selectedItemIndex = order[2];
let flashTimer = null;
let isAnimating = false;
let heldKey = null;
let repeatTimer = null;
let animationTimer = null;
function getCenterOffset() {
return {
x: Math.max(0, (window.innerWidth - 800) / 2),
y: Math.max(0, (window.innerHeight - 600) / 2)
};
}
function slotDelay(slotIndex, direction) {
// Direction-aware conveyor timing.
// On D/right, the right-side elements rise first, then the left side advances.
// On A/left, the belt mirrors that motion.
const d = settings.stagger;
const rightFirst = [0, d, d * 2, d * 3, d * 4];
const leftFirst = [d * 4, d * 3, d * 2, d, 0];
return direction === 1 ? rightFirst[slotIndex] : leftFirst[slotIndex];
}
function render({ teleportItemIndex = -1, direction = 1 } = {}) {
const { x: centerX, y: centerY } = getCenterOffset();
for (let slotIndex = 0; slotIndex < slots.length; slotIndex++) {
const itemIndex = order[slotIndex];
const el = els[itemIndex];
const slot = slots[slotIndex];
const isSelected = itemIndex === selectedItemIndex;
const x = centerX + slot.x;
const y = centerY + slot.y;
const delay = slotDelay(slotIndex, direction);
el.style.transition = `transform ${settings.moveDuration}ms cubic-bezier(.22,.86,.2,1) ${delay}ms, opacity 180ms ease, filter 180ms ease`;
el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(1)`;
el.style.zIndex = String(slot.z + (isSelected ? 100 : 0));
el.style.opacity = '1';
el.classList.toggle('selected', isSelected);
if (itemIndex === teleportItemIndex) {
// Instant wrap to the opposite side.
el.style.transition = 'none';
el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(1)`;
el.offsetHeight;
}
}
}
function step(direction) {
if (isAnimating) return;
isAnimating = true;
clearTimeout(animationTimer);
let teleportItemIndex = -1;
if (direction === 1) {
teleportItemIndex = order[0];
order = order.slice(1).concat(order[0]);
} else {
teleportItemIndex = order[order.length - 1];
order = [order[order.length - 1]].concat(order.slice(0, -1));
}
selectedItemIndex = order[2];
render({ teleportItemIndex, direction });
// Lock until the slowest element in the chain is done, so the queue
// cannot overlap or pass through itself.
const maxDelay = settings.stagger * 4;
animationTimer = window.setTimeout(() => {
isAnimating = false;
}, settings.moveDuration + maxDelay + 24);
}
function startHold(key) {
if (heldKey === key) return;
stopHold();
heldKey = key;
const direction = key === 'd' ? 1 : -1;
step(direction);
repeatTimer = window.setInterval(() => {
if (!isAnimating) step(direction);
}, settings.repeatDelay);
}
function stopHold() {
heldKey = null;
if (repeatTimer !== null) {
clearInterval(repeatTimer);
repeatTimer = null;
}
}
function flashSelected() {
clearTimeout(flashTimer);
els.forEach(el => el.classList.remove('selected'));
const selectedEl = els[selectedItemIndex];
if (!selectedEl) return;
selectedEl.classList.add('selected');
flashTimer = setTimeout(() => {
selectedEl.classList.remove('selected');
}, 3000);
}
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (key === 'a' || key === 'd') {
e.preventDefault();
if (!e.repeat) startHold(key);
} else if (e.key === 'Enter') {
flashSelected();
}
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (key === 'a' || key === 'd') stopHold();
});
window.addEventListener('blur', stopHold);
window.addEventListener('resize', () => render({ direction: 1 }));
stage.addEventListener('click', flashSelected);
render({ direction: 1 });
flashSelected();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment