Created
June 8, 2026 12:17
-
-
Save EncodeTheCode/1b9829db87dcc9882a4829329e7c1ad7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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