Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save EncodeTheCode/20a914d56c7588ef01b8aff5c70febfc to your computer and use it in GitHub Desktop.
Save EncodeTheCode/20a914d56c7588ef01b8aff5c70febfc 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" />
<title>CTR-style Power-up Spinner (Canvas Button)</title>
<style>
html,body{height:100%;margin:0;background:#222;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 Demo</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
const BOX_W = 165, BOX_H = 137;
// Default position (can be changed with setPosition)
let posX = 20.0, posY = 20.0;
// Button area (in CSS pixels)
const button = { x: 20, y: 180, w: 140, h: 40 };
// 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
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);
draw();
}
window.addEventListener('resize', resize);
resize();
// draw loop
function draw(){
// clear
ctx.clearRect(0,0,canvas.width/DPR, canvas.height/DPR);
// draw button
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
ctx.fillStyle = '#111';
roundRect(ctx, button.x, button.y, button.w, button.h, 8, true, false);
// border
ctx.lineWidth = 2;
ctx.strokeStyle = '#fff4';
roundRect(ctx, button.x+1, button.y+1, button.w-2, button.h-2, 6, false, true);
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', button.x + button.w/2, button.y + button.h/2);
ctx.restore();
}
function drawBox(){
ctx.save();
// semi-transparent black rectangle (rounded as before)
ctx.fillStyle = 'rgba(0,0,0,0.67)';
roundRect(ctx, posX, posY, BOX_W, BOX_H, 8, true, false);
// border
ctx.lineWidth = 2;
ctx.strokeStyle = '#FFFFFF55';
roundRect(ctx, posX+1, posY+1, BOX_W-2, BOX_H-2, 6, false, true);
// draw item area - SHARP box (no rounded corners)
const padding = 8;
const iconSize = Math.min(BOX_W - padding*2, BOX_H - padding*2 - 28);
const ix = posX + (BOX_W - iconSize)/2;
const iy = posY + padding + 6;
// sharp item background box
ctx.fillStyle = '#222';
ctx.fillRect(Math.round(ix-4), Math.round(iy-4), Math.round(iconSize+8), Math.round(iconSize+8));
ctx.lineWidth = 1;
ctx.strokeStyle = '#333';
ctx.strokeRect(Math.round(ix-4)+0.5, Math.round(iy-4)+0.5, Math.round(iconSize+8), Math.round(iconSize+8));
if(items.length>0){
const it = items[currentIndex % items.length];
// draw item stretched to cover the item area but allow it to overlap outside slightly
const overlapFactor = 1.15; // 15% larger to allow overlap
const drawSize = iconSize * overlapFactor;
const dx = ix + (iconSize - drawSize) / 2;
const dy = iy + (iconSize - drawSize) / 2;
if(it.type === 'color'){
// stretched color rectangle (may overlap outside the sharp box)
ctx.fillStyle = it.value;
ctx.fillRect(dx, dy, drawSize, drawSize);
} else if(it.type === 'image'){
const img = imageCache.get(it.value);
if(img && img.complete){
// draw image stretched (might overlap outside box)
ctx.drawImage(img, dx, dy, drawSize, drawSize);
} else {
ctx.fillStyle = '#666';
ctx.fillRect(dx, dy, drawSize, drawSize);
}
}
}
// label text
ctx.fillStyle = '#fff';
ctx.font = '12px system-ui,Segoe UI,Roboto';
ctx.textAlign = 'center';
ctx.fillText('Power-up', posX + BOX_W/2, posY + BOX_H - 18);
// if selected and waiting for use, show hint
if(state === 'selected'){
ctx.font = '11px system-ui,Segoe UI,Roboto';
ctx.fillText('Press E to use', posX + BOX_W/2, posY + BOX_H - 6);
}
ctx.restore();
}
function roundRect(ctx, x, y, w, h, r, fill, stroke){
if(typeof r === 'undefined') r = 5;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
if(fill) ctx.fill();
if(stroke) ctx.stroke();
}
// 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, button)){
// 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
function setPosition(x,y){ posX = +x; posY = +y; 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 spinner (canvas) 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