421 lines
13 KiB
JavaScript
421 lines
13 KiB
JavaScript
// Randomizer Wheel Easter Egg
|
|
// A fun decision-making tool hidden in the Tools page
|
|
|
|
let wheel = {
|
|
canvas: null,
|
|
ctx: null,
|
|
spinning: false,
|
|
rotation: 0,
|
|
targetRotation: 0,
|
|
spinSpeed: 0,
|
|
spinStartTime: 0,
|
|
options: [],
|
|
colors: [],
|
|
currentPreset: 'decisions'
|
|
};
|
|
|
|
// Preset configurations
|
|
const presets = {
|
|
decisions: {
|
|
name: 'Decisions',
|
|
options: ['Yes!', 'No Way', 'Maybe', 'Ask Again', 'Definitely', 'Not Now', 'Go For It!', 'Wait'],
|
|
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2']
|
|
},
|
|
lunch: {
|
|
name: 'Lunch Ideas',
|
|
options: ['Pizza 🍕', 'Burgers 🍔', 'Sushi 🍣', 'Tacos 🌮', 'Salad 🥗', 'Pasta 🍝', 'Chinese 🥡', 'Sandwiches 🥪'],
|
|
colors: ['#E74C3C', '#F39C12', '#3498DB', '#2ECC71', '#9B59B6', '#1ABC9C', '#E67E22', '#34495E']
|
|
},
|
|
tasks: {
|
|
name: 'Task Priority',
|
|
options: ['Do It Now!', 'Schedule It', 'Delegate', 'Skip It', 'High Priority', 'Low Priority', 'Break Time!', 'Focus Time'],
|
|
colors: ['#FF4757', '#FFA502', '#2ED573', '#1E90FF', '#5F27CD', '#00D2D3', '#FF6348', '#C23616']
|
|
},
|
|
colors: {
|
|
name: 'Powder Colors',
|
|
options: ['Gloss Black', 'Candy Red', 'Chrome Silver', 'Matte White', 'Metallic Blue', 'Neon Green', 'Rose Gold', 'Deep Purple'],
|
|
colors: ['#000000', '#DC143C', '#C0C0C0', '#F5F5F5', '#4169E1', '#39FF14', '#B76E79', '#663399']
|
|
}
|
|
};
|
|
|
|
// Initialize the wheel when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
wheel.canvas = document.getElementById('wheelCanvas');
|
|
if (wheel.canvas) {
|
|
wheel.ctx = wheel.canvas.getContext('2d');
|
|
setWheelPreset('decisions');
|
|
|
|
// Add click event to Tools header
|
|
const header = document.getElementById('toolsHeader');
|
|
if (header) {
|
|
header.addEventListener('click', function() {
|
|
const modal = new bootstrap.Modal(document.getElementById('randomizerModal'));
|
|
modal.show();
|
|
|
|
// Redraw wheel when modal opens (in case it was resized)
|
|
setTimeout(() => drawWheel(), 100);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Set wheel to a preset configuration
|
|
function setWheelPreset(presetName) {
|
|
const preset = presets[presetName];
|
|
if (!preset) return;
|
|
|
|
wheel.options = [...preset.options];
|
|
wheel.colors = [...preset.colors];
|
|
wheel.currentPreset = presetName;
|
|
|
|
// Update button states
|
|
const buttons = document.querySelectorAll('.btn-group .btn');
|
|
buttons.forEach((btn, index) => {
|
|
btn.classList.remove('active');
|
|
// Set active based on preset name
|
|
const btnText = btn.textContent.toLowerCase();
|
|
if (btnText.includes(presetName)) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// Clear result
|
|
const resultDiv = document.getElementById('result');
|
|
if (resultDiv) {
|
|
resultDiv.innerHTML = '';
|
|
}
|
|
|
|
// Redraw wheel
|
|
drawWheel();
|
|
}
|
|
|
|
// Load shop workers from the server
|
|
async function loadShopWorkers() {
|
|
try {
|
|
// Show loading state
|
|
const resultDiv = document.getElementById('result');
|
|
if (resultDiv) {
|
|
resultDiv.innerHTML = '<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading workers...</span></div>';
|
|
}
|
|
|
|
const response = await fetch('/Tools/GetShopWorkers');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.workers && data.workers.length > 0) {
|
|
wheel.options = data.workers;
|
|
wheel.colors = generateRainbowColors(data.workers.length);
|
|
wheel.currentPreset = 'workers';
|
|
|
|
// Update button states
|
|
const buttons = document.querySelectorAll('.btn-group .btn');
|
|
buttons.forEach(btn => {
|
|
btn.classList.remove('active');
|
|
if (btn.textContent.includes('Shop Workers')) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// Clear result and redraw
|
|
if (resultDiv) {
|
|
resultDiv.innerHTML = `<small class="text-success"><i class="bi bi-check-circle me-1"></i>Loaded ${data.workers.length} active shop workers</small>`;
|
|
}
|
|
drawWheel();
|
|
} else {
|
|
if (resultDiv) {
|
|
resultDiv.innerHTML = '<small class="text-warning"><i class="bi bi-exclamation-triangle me-1"></i>No active shop workers found</small>';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading shop workers:', error);
|
|
const resultDiv = document.getElementById('result');
|
|
if (resultDiv) {
|
|
resultDiv.innerHTML = '<small class="text-danger"><i class="bi bi-x-circle me-1"></i>Error loading shop workers</small>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set custom options from textarea
|
|
function setCustomOptions() {
|
|
const input = document.getElementById('customOptionsInput');
|
|
const options = input.value
|
|
.split('\n')
|
|
.map(opt => opt.trim())
|
|
.filter(opt => opt.length > 0);
|
|
|
|
if (options.length < 2) {
|
|
showWarning('Please enter at least 2 options!', 'Invalid Input');
|
|
return;
|
|
}
|
|
|
|
wheel.options = options;
|
|
|
|
// Generate rainbow colors for custom options
|
|
wheel.colors = generateRainbowColors(options.length);
|
|
|
|
// Collapse the custom options panel
|
|
const collapse = bootstrap.Collapse.getInstance(document.getElementById('customOptions'));
|
|
if (collapse) collapse.hide();
|
|
|
|
// Clear result and redraw
|
|
document.getElementById('result').innerHTML = '';
|
|
drawWheel();
|
|
}
|
|
|
|
// Generate rainbow colors
|
|
function generateRainbowColors(count) {
|
|
const colors = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const hue = (i * 360) / count;
|
|
colors.push(`hsl(${hue}, 70%, 60%)`);
|
|
}
|
|
return colors;
|
|
}
|
|
|
|
// Draw the wheel
|
|
function drawWheel() {
|
|
if (!wheel.ctx) return;
|
|
|
|
const canvas = wheel.canvas;
|
|
const ctx = wheel.ctx;
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
const radius = Math.min(centerX, centerY) - 10;
|
|
|
|
// Clear canvas
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Save context
|
|
ctx.save();
|
|
|
|
// Rotate canvas
|
|
ctx.translate(centerX, centerY);
|
|
ctx.rotate(wheel.rotation);
|
|
ctx.translate(-centerX, -centerY);
|
|
|
|
const numOptions = wheel.options.length;
|
|
const anglePerOption = (2 * Math.PI) / numOptions;
|
|
|
|
// Draw each segment
|
|
for (let i = 0; i < numOptions; i++) {
|
|
const startAngle = i * anglePerOption - Math.PI / 2;
|
|
const endAngle = (i + 1) * anglePerOption - Math.PI / 2;
|
|
|
|
// Draw segment
|
|
ctx.beginPath();
|
|
ctx.moveTo(centerX, centerY);
|
|
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
|
ctx.closePath();
|
|
|
|
ctx.fillStyle = wheel.colors[i % wheel.colors.length];
|
|
ctx.fill();
|
|
|
|
// Draw border
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
|
|
// Draw text
|
|
ctx.save();
|
|
ctx.translate(centerX, centerY);
|
|
ctx.rotate(startAngle + anglePerOption / 2);
|
|
ctx.textAlign = 'right';
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.font = 'bold 18px Arial';
|
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
|
|
ctx.shadowBlur = 3;
|
|
ctx.fillText(wheel.options[i], radius - 20, 8);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Draw center circle
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, 30, 0, 2 * Math.PI);
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#333333';
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
|
|
// Draw center star
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.font = 'bold 24px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText('★', centerX, centerY);
|
|
|
|
// Restore context
|
|
ctx.restore();
|
|
}
|
|
|
|
// Spin the wheel
|
|
function spinWheel() {
|
|
if (wheel.spinning) return;
|
|
|
|
wheel.spinning = true;
|
|
wheel.spinStartTime = Date.now();
|
|
document.getElementById('spinBtn').disabled = true;
|
|
document.getElementById('result').innerHTML = '<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Spinning...</span></div>';
|
|
|
|
// Random spin: 5-8 full rotations + random position
|
|
const fullRotations = 5 + Math.random() * 3;
|
|
const randomAngle = Math.random() * 2 * Math.PI;
|
|
wheel.targetRotation = wheel.rotation + (fullRotations * 2 * Math.PI) + randomAngle;
|
|
wheel.spinSpeed = 0.3;
|
|
|
|
animateSpin();
|
|
}
|
|
|
|
// Animate the spin
|
|
function animateSpin() {
|
|
if (!wheel.spinning) return;
|
|
|
|
// Safety timeout: stop after 10 seconds
|
|
const elapsed = Date.now() - wheel.spinStartTime;
|
|
if (elapsed > 10000) {
|
|
console.warn('Spin animation timed out, forcing stop');
|
|
wheel.rotation = wheel.targetRotation;
|
|
finishSpin();
|
|
return;
|
|
}
|
|
|
|
// Ease out
|
|
const distance = wheel.targetRotation - wheel.rotation;
|
|
|
|
// Stop if we're very close OR if the speed is too slow to make progress
|
|
if (Math.abs(distance) < 0.01 || wheel.spinSpeed < 0.001) {
|
|
wheel.rotation = wheel.targetRotation;
|
|
finishSpin();
|
|
return;
|
|
}
|
|
|
|
wheel.rotation += distance * wheel.spinSpeed;
|
|
wheel.spinSpeed *= 0.97; // Deceleration
|
|
|
|
drawWheel();
|
|
requestAnimationFrame(animateSpin);
|
|
}
|
|
|
|
// Finish the spin and show result
|
|
function finishSpin() {
|
|
// Normalize rotation to 0-2π for next spin
|
|
wheel.rotation = wheel.rotation % (2 * Math.PI);
|
|
if (wheel.rotation < 0) wheel.rotation += 2 * Math.PI;
|
|
|
|
wheel.spinning = false;
|
|
document.getElementById('spinBtn').disabled = false;
|
|
showResult();
|
|
}
|
|
|
|
// Show the result
|
|
function showResult() {
|
|
const numOptions = wheel.options.length;
|
|
const anglePerOption = (2 * Math.PI) / numOptions;
|
|
|
|
// Normalize rotation to 0-2π
|
|
let normalizedRotation = wheel.rotation % (2 * Math.PI);
|
|
if (normalizedRotation < 0) normalizedRotation += 2 * Math.PI;
|
|
|
|
// Calculate which segment is at the pointer (top of wheel)
|
|
// Canvas rotation is counterclockwise, so we need to reverse the calculation
|
|
const rawIndex = (2 * Math.PI - normalizedRotation) / anglePerOption;
|
|
const selectedIndex = Math.floor(rawIndex) % numOptions;
|
|
|
|
const winner = wheel.options[selectedIndex];
|
|
const winnerColor = wheel.colors[selectedIndex % wheel.colors.length];
|
|
|
|
// Display result with animation
|
|
const resultDiv = document.getElementById('result');
|
|
resultDiv.innerHTML = `
|
|
<div class="alert alert-success border-3" style="border-color: ${winnerColor} !important; animation: fadeInScale 0.5s ease;">
|
|
<h4 class="mb-2">
|
|
<i class="bi bi-trophy-fill text-warning me-2"></i>Winner!
|
|
</h4>
|
|
<h3 class="mb-0" style="color: ${winnerColor}; font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
|
|
${winner}
|
|
</h3>
|
|
</div>
|
|
`;
|
|
|
|
// Add confetti effect
|
|
createConfetti();
|
|
}
|
|
|
|
// Create confetti effect
|
|
function createConfetti() {
|
|
const duration = 2000;
|
|
const animationEnd = Date.now() + duration;
|
|
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F'];
|
|
|
|
(function frame() {
|
|
const timeLeft = animationEnd - Date.now();
|
|
|
|
if (timeLeft <= 0) return;
|
|
|
|
const particleCount = 2;
|
|
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const particle = document.createElement('div');
|
|
particle.style.position = 'fixed';
|
|
particle.style.width = '10px';
|
|
particle.style.height = '10px';
|
|
particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
|
particle.style.borderRadius = '50%';
|
|
particle.style.pointerEvents = 'none';
|
|
particle.style.zIndex = '9999';
|
|
|
|
const startX = Math.random() * window.innerWidth;
|
|
const startY = -20;
|
|
|
|
particle.style.left = startX + 'px';
|
|
particle.style.top = startY + 'px';
|
|
|
|
document.body.appendChild(particle);
|
|
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const velocity = 2 + Math.random() * 2;
|
|
const vx = Math.cos(angle) * velocity;
|
|
const vy = Math.sin(angle) * velocity + 3;
|
|
|
|
let x = startX;
|
|
let y = startY;
|
|
let opacity = 1;
|
|
|
|
const animate = () => {
|
|
y += vy;
|
|
x += vx;
|
|
opacity -= 0.02;
|
|
|
|
particle.style.top = y + 'px';
|
|
particle.style.left = x + 'px';
|
|
particle.style.opacity = opacity;
|
|
|
|
if (opacity > 0 && y < window.innerHeight) {
|
|
requestAnimationFrame(animate);
|
|
} else {
|
|
particle.remove();
|
|
}
|
|
};
|
|
|
|
animate();
|
|
}
|
|
|
|
requestAnimationFrame(frame);
|
|
})();
|
|
}
|
|
|
|
// Add CSS animation for result
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes fadeInScale {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.8);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|