Created
October 3, 2025 15:01
-
-
Save EncodeTheCode/80b9969fafa85dcccd33ee91952f3c68 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" /> | |
<title>CTR-style Power-up Spinner (Canvas Button - Sharp)</title> | |
<style> | |
html,body{height:100%;margin:0;background:#FFFFFF;color:#ddd;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial} | |
#gameCanvas{display:block;width:100%;height:100%} | |
.info{position:fixed;left:8px;top:8px;background:rgba(0,0,0,0.5);padding:8px;border-radius:6px;font-size:13px} | |
</style> | |
</head> | |
<body> | |
<canvas id="gameCanvas"></canvas> | |
<div class="info"> | |
<div><strong>CTR-style Power-up Spinner — Canvas Button (Sharp)</strong></div> | |
<div style="opacity:0.85;font-size:12px;margin-top:6px">Click the canvas <em>Break Crate</em> button or call <code>break_special_crate()</code>. Press <kbd>B</kbd> to force-select after 0.5s of spinning. When an item is selected it stays on screen until you press <kbd>E</kbd> to use it. After using the item it blinks rapidly for 2500ms and disappears.</div> | |
</div> | |
<script> | |
(()=>{ | |
const canvas = document.getElementById('gameCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const DPR = Math.max(1, window.devicePixelRatio || 1); | |
// Box size (fixed) | |
const BOX_W = 165, BOX_H = 137; | |
// Default auto-centered position (top center, 30px from top) | |
let posX = 0, posY = 30; | |
let autoCentered = true; | |
// Button size (we'll position it under the spinner) | |
const buttonSize = { w: 137, h: 22 }; | |
let buttonRect = { x: 0, y: 0, w: buttonSize.w, h: buttonSize.h }; | |
// Items (colors by default) | |
let items = [ | |
{type:'color', value:'#E53935'}, | |
{type:'color', value:'#FFB300'}, | |
{type:'color', value:'#8E24AA'}, | |
{type:'color', value:'#1E88E5'}, | |
{type:'color', value:'#43A047'}, | |
{type:'color', value:'#F06292'}, | |
{type:'color', value:'#FF7043'}, | |
{type:'color', value:'#7E57C2'} | |
]; | |
let imageCache = new Map(); | |
// Spinner states: 'hidden', 'spinning', 'selected', 'using_blink' | |
let state = 'hidden'; | |
let currentIndex = 0; | |
let spinInterval = null; | |
let spinStart = 0; | |
// timings | |
const FAST_SWITCH_MS = 80; | |
const AUTO_SELECT_MS = 5000; | |
const MIN_B_PRESS_MS = 500; | |
const USE_BLINK_MS = 2500; // blink duration when using item (2.5 seconds) | |
const USE_BLINK_INTERVAL_MS = 60; // blink frequency (smaller = faster blinking) | |
// blink helpers for using | |
let usingBlinkStart = 0; | |
let usingBlinkVisible = true; | |
let usingBlinkTimer = null; | |
// Auto select timer handle | |
let autoSelectTimer = null; | |
// Resize canvas and auto-center if enabled | |
function resize(){ | |
canvas.width = Math.floor(window.innerWidth * DPR); | |
canvas.height = Math.floor(window.innerHeight * DPR); | |
canvas.style.width = window.innerWidth + 'px'; | |
canvas.style.height = window.innerHeight + 'px'; | |
ctx.setTransform(DPR,0,0,DPR,0,0); | |
if(autoCentered){ | |
posX = Math.round((window.innerWidth - BOX_W) / 2); | |
posY = 30; // 30px from top as requested | |
} | |
// position button under the spinner, centered | |
buttonRect.w = buttonSize.w; buttonRect.h = buttonSize.h; | |
buttonRect.x = Math.round(posX + (BOX_W - buttonRect.w)/2); | |
buttonRect.y = Math.round(posY + BOX_H + 10); | |
draw(); | |
} | |
window.addEventListener('resize', resize); | |
resize(); | |
// draw loop | |
function draw(){ | |
// clear | |
ctx.clearRect(0,0,canvas.width/DPR, canvas.height/DPR); | |
// draw button (sharp rect) | |
drawButton(); | |
// draw spinner box only when state != 'hidden' | |
if(state !== 'hidden'){ | |
// if using_blink, respect usingBlinkVisible | |
if(state === 'using_blink' && !usingBlinkVisible) return; | |
drawBox(); | |
} | |
} | |
function drawButton(){ | |
ctx.save(); | |
// button background - sharp rectangle | |
ctx.fillStyle = '#111'; | |
ctx.fillRect(buttonRect.x, buttonRect.y, buttonRect.w, buttonRect.h); | |
// border | |
ctx.lineWidth = 2; | |
ctx.strokeStyle = '#FFFFFF44'; | |
ctx.strokeRect(buttonRect.x + 0.5, buttonRect.y + 0.5, buttonRect.w, buttonRect.h); | |
ctx.fillStyle = '#fff'; | |
ctx.font = '16px system-ui,Segoe UI,Roboto'; | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillText(state === 'hidden' ? 'Break Crate' : 'Crate Active', buttonRect.x + buttonRect.w/2, buttonRect.y + buttonRect.h/2); | |
ctx.restore(); | |
} | |
function drawBox(){ | |
ctx.save(); | |
// outer container: sharp rectangle fixed to BOX_W x BOX_H | |
ctx.fillStyle = 'rgba(33,33,33,0.5)'; | |
ctx.fillRect(posX, posY, BOX_W, BOX_H); | |
ctx.lineWidth = 10; | |
ctx.strokeStyle = '#000000F5'; | |
ctx.strokeRect(posX + 0.5, posY + 0.5, BOX_W, BOX_H); | |
// tightly confined inner sharp item box. | |
const innerMargin = 0; // tight margin | |
const labelHeight = 0; // removed label text as requested | |
// compute the item area to tightly fill the box minus margins | |
const ix = posX + innerMargin; | |
const iy = posY + innerMargin; | |
const iw = BOX_W - innerMargin*2; | |
const ih = BOX_H - innerMargin*2 - labelHeight; | |
// draw sharp (non-rounded) item background that tightly fits inside | |
ctx.fillStyle = '#222'; | |
ctx.fillRect(ix, iy, iw, ih); | |
ctx.lineWidth = 1; | |
ctx.strokeStyle = '#333'; | |
ctx.strokeRect(ix + 0.5, iy + 0.5, iw, ih); | |
// draw item stretched exactly to the confined area (no overlap outside) | |
if(items.length>0){ | |
const it = items[currentIndex % items.length]; | |
if(it.type === 'color'){ | |
ctx.fillStyle = it.value; | |
ctx.fillRect(ix, iy, iw, ih); | |
} else if(it.type === 'image'){ | |
const img = imageCache.get(it.value); | |
if(img && img.complete){ | |
ctx.drawImage(img, ix, iy, iw, ih); | |
} else { | |
ctx.fillStyle = '#666'; | |
ctx.fillRect(ix, iy, iw, ih); | |
} | |
} | |
} | |
ctx.restore(); | |
} | |
// Start spinning — called by button or API | |
function break_special_crate(){ | |
if(state !== 'hidden') return; // only allow when hidden | |
if(!items || items.length === 0) return; | |
// preload images | |
items.forEach(it => { if(it.type === 'image' && it.value && !imageCache.has(it.value)){ | |
const img = new Image(); img.src = it.value; imageCache.set(it.value, img); | |
}}); | |
state = 'spinning'; | |
spinStart = performance.now(); | |
currentIndex = Math.floor(Math.random() * items.length); | |
// start fast cycling | |
spinInterval = setInterval(()=>{ | |
currentIndex = (currentIndex + 1) % items.length; | |
draw(); | |
}, FAST_SWITCH_MS); | |
// set up auto-select | |
if(autoSelectTimer) clearTimeout(autoSelectTimer); | |
autoSelectTimer = setTimeout(()=>{ | |
if(state === 'spinning') selectCurrent(); | |
}, AUTO_SELECT_MS); | |
draw(); | |
return { startedAt: spinStart }; | |
} | |
// select current item and stop cycling - but do NOT blink or hide until E pressed | |
function selectCurrent(){ | |
if(state !== 'spinning') return; | |
// stop cycling | |
if(spinInterval){ clearInterval(spinInterval); spinInterval = null; } | |
if(autoSelectTimer){ clearTimeout(autoSelectTimer); autoSelectTimer = null; } | |
const selected = { index: currentIndex % items.length, item: items[currentIndex % items.length] }; | |
// dispatch selection event | |
window.dispatchEvent(new CustomEvent('specialCrateSelected', { detail: selected })); | |
// set to selected state and keep visible until used | |
state = 'selected'; | |
draw(); | |
} | |
// when player presses E to use the currently selected item | |
function useSelected(){ | |
if(state !== 'selected') return; | |
state = 'using_blink'; | |
usingBlinkStart = performance.now(); | |
usingBlinkVisible = true; | |
if(usingBlinkTimer) clearInterval(usingBlinkTimer); | |
usingBlinkTimer = setInterval(()=>{ | |
const elapsed = performance.now() - usingBlinkStart; | |
usingBlinkVisible = ((Math.floor(elapsed / USE_BLINK_INTERVAL_MS) % 2) === 0); | |
if(elapsed >= USE_BLINK_MS){ | |
clearInterval(usingBlinkTimer); usingBlinkTimer = null; | |
// finish use: hide and reset | |
state = 'hidden'; | |
currentIndex = 0; | |
draw(); | |
} else { | |
draw(); | |
} | |
}, Math.max(6, Math.floor(USE_BLINK_INTERVAL_MS/2))); | |
// ensure final cleanup | |
setTimeout(()=>{ | |
if(usingBlinkTimer){ clearInterval(usingBlinkTimer); usingBlinkTimer = null; } | |
state = 'hidden'; | |
currentIndex = 0; | |
draw(); | |
}, USE_BLINK_MS + 50); | |
} | |
// keyboard input for B (select early) and E (use) | |
window.addEventListener('keydown', (e)=>{ | |
if(e.key === 'b' || e.key === 'B'){ | |
if(state === 'spinning'){ | |
const elapsed = performance.now() - spinStart; | |
if(elapsed >= MIN_B_PRESS_MS) selectCurrent(); | |
} | |
} else if(e.key === 'e' || e.key === 'E'){ | |
if(state === 'selected'){ | |
useSelected(); | |
} | |
} | |
}); | |
// mouse handling for button click | |
canvas.addEventListener('pointerdown', (ev)=>{ | |
const rect = canvas.getBoundingClientRect(); | |
const x = (ev.clientX - rect.left); // CSS pixels | |
const y = (ev.clientY - rect.top); | |
// check button (button coordinates are in CSS pixels) | |
if(pointInRect(x, y, buttonRect)){ | |
// simulate break | |
break_special_crate(); | |
return; | |
} | |
}); | |
function pointInRect(x,y,r){ return x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h; } | |
// API: setPosition(x,y) places the spinner box and disables auto-centering | |
function setPosition(x,y){ posX = +x; posY = +y; autoCentered = false; // update button too | |
buttonRect.x = Math.round(posX + (BOX_W - buttonRect.w)/2); | |
buttonRect.y = Math.round(posY + BOX_H + 10); | |
draw(); } | |
function setItems(arr){ if(!Array.isArray(arr)) throw new Error('setItems expects an array'); items = arr.slice(); items.forEach(it=>{ if(it.type==='image' && it.value && !imageCache.has(it.value)){ const img=new Image(); img.src=it.value; imageCache.set(it.value,img);} }); currentIndex = 0; draw(); } | |
function isSpinnerActive(){ return state === 'spinning' || state === 'selected' || state === 'using_blink'; } | |
// expose functions | |
window.break_special_crate = break_special_crate; | |
window.setPosition = setPosition; | |
window.setItems = setItems; | |
window.isSpinnerActive = isSpinnerActive; | |
// initial draw | |
draw(); | |
console.log('CTR-style sharp spinner ready. Use break_special_crate(), setPosition(x,y), setItems(array). Press B to select early while spinning, press E to use the selected item. Listen for "specialCrateSelected" on window.'); | |
})(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment