Skip to content

Commit d7d5580

Browse files
committed
- add configurable spin duration (1s-10s) with localStorage persistence
- replace physics-based animation with time-based `easeOut()` curve - implement realistic `SpinningWheelSound` class with dynamic click timing
1 parent ca2516d commit d7d5580

File tree

1 file changed

+132
-69
lines changed

1 file changed

+132
-69
lines changed

tools/decision_wheel.html

Lines changed: 132 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,32 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5-
<meta name="description" content="Free online Decision Wheel - Spin the wheel to make random choices! Add your options and let our customizable spinning wheel pick for you. Perfect for decisions, games, and random selection.">
6-
<meta name="keywords" content="decision wheel, spinning wheel, random picker, choice maker, decision maker, random selector, spin wheel, online wheel, random choice, decision tool, picker wheel, fortune wheel">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Decision Wheel - Random Picker</title>
7+
<meta name="description"
8+
content="Free online Decision Wheel - Spin the wheel to make random choices! Add your options and let our customizable spinning wheel pick for you. Perfect for decisions, games, and random selection.">
9+
<meta name="keywords"
10+
content="decision wheel, spinning wheel, random picker, choice maker, decision maker, random selector, spin wheel, online wheel, random choice, decision tool, picker wheel, fortune wheel">
711
<meta name="author" content="Claude Sonnet 4 prompted by Tobias Müller">
812
<meta name="robots" content="index, follow">
9-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1013
<link rel="canonical" href="https://www.gptgames.dev/tools/decision_wheel.html">
11-
1214
<meta property="og:title" content="Decision Wheel - Random Picker Tool">
13-
<meta property="og:description" content="Free online Decision Wheel - Spin the wheel to make random choices! Add your options and let our customizable spinning wheel pick for you. Perfect for decisions, games, and random selection.">
15+
<meta property="og:description"
16+
content="Free online Decision Wheel - Spin the wheel to make random choices! Add your options and let our customizable spinning wheel pick for you. Perfect for decisions, games, and random selection.">
1417
<meta property="og:image" content="https://www.gptgames.dev/screenshots/screenshot_208.webp">
1518
<meta property="og:url" content="https://www.gptgames.dev/tools/decision_wheel.html">
1619
<meta property="og:type" content="website">
1720
<meta property="og:site_name" content="GPT Games">
18-
1921
<meta name="twitter:card" content="summary_large_image">
2022
<meta name="twitter:title" content="Decision Wheel - Random Picker Tool">
21-
<meta name="twitter:description" content="Free online Decision Wheel - Spin the wheel to make random choices! Add your options and let our customizable spinning wheel pick for you.">
23+
<meta name="twitter:description"
24+
content="Free online Decision Wheel - Spin the wheel to make random choices! Add your options and let our customizable spinning wheel pick for you.">
2225
<meta name="twitter:image" content="https://www.gptgames.dev/screenshots/screenshot_208.webp">
23-
2426
<meta name="theme-color" content="#3182ce">
2527
<meta name="application-name" content="Decision Wheel">
2628
<meta name="apple-mobile-web-app-title" content="Decision Wheel">
2729
<meta name="apple-mobile-web-app-capable" content="yes">
2830
<meta name="apple-mobile-web-app-status-bar-style" content="default">
29-
<title>Decision Wheel - Random Picker</title>
3031
<style>
3132
* {
3233
margin: 0;
@@ -193,7 +194,7 @@
193194
font-size: 0.9rem
194195
}
195196

196-
.form-group input {
197+
.form-group input, .form-group select {
197198
width: 100%;
198199
padding: 0.75rem 1rem;
199200
border: 2px solid #e5e7eb;
@@ -203,12 +204,18 @@
203204
background: white
204205
}
205206

206-
.form-group input:focus {
207+
.form-group input:focus, .form-group select:focus {
207208
outline: none;
208209
border-color: #3182ce;
209210
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1)
210211
}
211212

213+
.duration-info {
214+
font-size: 0.8rem;
215+
color: #6b7280;
216+
margin-top: 0.25rem
217+
}
218+
212219
.btn {
213220
padding: 0.7rem 1.2rem;
214221
border: none;
@@ -473,9 +480,20 @@ <h3>Wheel Options</h3>
473480
</div>
474481
<div class="settings-section">
475482
<h4>Settings</h4>
483+
<div class="form-group">
484+
<label for="spinDuration">Spin Duration</label>
485+
<select id="spinDuration">
486+
<option value="1">⚡ Instant (1s) - Perfect for streaming</option>
487+
<option value="2">🏃 Fast (2s) - Quick decision</option>
488+
<option value="4" selected>⚖️ Normal (4s) - Balanced</option>
489+
<option value="6">🎪 Dramatic (6s) - Build suspense</option>
490+
<option value="10">🐌 Slow (10s) - Maximum drama</option>
491+
</select>
492+
<div class="duration-info">Choose based on your use case - streamers prefer faster spins</div>
493+
</div>
476494
<div class="checkbox-group">
477495
<input type="checkbox" id="soundToggle" checked>
478-
<label for="soundToggle">Sound effects</label>
496+
<label for="soundToggle">Spinning sound</label>
479497
</div>
480498
<div class="checkbox-group">
481499
<input type="checkbox" id="confettiToggle" checked>
@@ -493,6 +511,65 @@ <h4>Recent Results</h4>
493511
</div>
494512
</div>
495513
<script>
514+
class SpinningWheelSound {
515+
constructor() {
516+
try {
517+
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
518+
this.playing = false;
519+
} catch (e) {
520+
this.ctx = null;
521+
}
522+
}
523+
524+
click(when = 0) {
525+
if (!this.ctx) return;
526+
const t = this.ctx.currentTime + when;
527+
const osc = this.ctx.createOscillator();
528+
const gain = this.ctx.createGain();
529+
const filter = this.ctx.createBiquadFilter();
530+
531+
osc.frequency.setValueAtTime(2800, t);
532+
osc.frequency.exponentialRampToValueAtTime(800, t + 0.015);
533+
filter.type = 'highpass';
534+
filter.frequency.value = 400;
535+
gain.gain.setValueAtTime(0, t);
536+
gain.gain.linearRampToValueAtTime(0.4, t + 0.003);
537+
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.06);
538+
539+
osc.connect(filter).connect(gain).connect(this.ctx.destination);
540+
osc.start(t);
541+
osc.stop(t + 0.06);
542+
}
543+
544+
spin(duration = 3000, initialSpeed = 30) {
545+
if (!this.ctx || this.playing) return;
546+
this.playing = true;
547+
const startTime = Date.now();
548+
const minSpeed = 1;
549+
550+
const loop = () => {
551+
const elapsed = Date.now() - startTime;
552+
if (elapsed > duration) {
553+
this.playing = false;
554+
return;
555+
}
556+
557+
this.click();
558+
559+
// Calculate current speed based on time progress (exponential curve)
560+
const progress = elapsed / duration;
561+
const currentSpeed = initialSpeed * Math.pow(minSpeed / initialSpeed, progress);
562+
563+
setTimeout(loop, 1000 / currentSpeed);
564+
};
565+
loop();
566+
}
567+
568+
stop() {
569+
this.playing = false;
570+
}
571+
}
572+
496573
class DecisionWheel {
497574
constructor() {
498575
this.canvas = document.getElementById('wheel');
@@ -501,12 +578,14 @@ <h4>Recent Results</h4>
501578
this.colors = ['#3182ce', '#38a169', '#e53e3e', '#d69e2e', '#805ad5', '#dd6b20', '#319795', '#e53e3e', '#38b2ac', '#3182ce'];
502579
this.rotation = 0;
503580
this.spinning = false;
504-
this.spinSpeed = 0;
505-
this.friction = 0.996;
506-
this.minSpeed = 0.008;
581+
this.spinStartTime = 0;
582+
this.spinDuration = parseInt(localStorage.getItem('spinDuration')) || 4;
583+
this.startRotation = 0;
584+
this.targetRotation = 0;
507585
this.history = JSON.parse(localStorage.getItem('wheelHistory')) || [];
508586
this.soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
509587
this.confettiEnabled = localStorage.getItem('confettiEnabled') !== 'false';
588+
this.sound = new SpinningWheelSound();
510589
this.setupEventListeners();
511590
this.updateDisplay();
512591
this.draw()
@@ -519,6 +598,7 @@ <h4>Recent Results</h4>
519598
const resetBtn = document.getElementById('resetBtn');
520599
const soundToggle = document.getElementById('soundToggle');
521600
const confettiToggle = document.getElementById('confettiToggle');
601+
const spinDurationSelect = document.getElementById('spinDuration');
522602
addBtn.addEventListener('click', () => this.addSegment());
523603
newSegmentInput.addEventListener('keypress', e => {
524604
if (e.key === 'Enter' && !e.shiftKey) {
@@ -536,11 +616,16 @@ <h4>Recent Results</h4>
536616
this.confettiEnabled = e.target.checked;
537617
localStorage.setItem('confettiEnabled', this.confettiEnabled)
538618
});
619+
spinDurationSelect.addEventListener('change', e => {
620+
this.spinDuration = parseInt(e.target.value);
621+
localStorage.setItem('spinDuration', this.spinDuration)
622+
});
539623
this.canvas.addEventListener('click', () => {
540624
if (!this.spinning && this.segments.length >= 2) this.spin()
541625
});
542626
soundToggle.checked = this.soundEnabled;
543-
confettiToggle.checked = this.confettiEnabled
627+
confettiToggle.checked = this.confettiEnabled;
628+
spinDurationSelect.value = this.spinDuration
544629
}
545630

546631
addSegment() {
@@ -626,30 +711,43 @@ <h4>Recent Results</h4>
626711
spin() {
627712
if (this.spinning || this.segments.length < 2) return;
628713
this.spinning = true;
629-
this.spinSpeed = 0.25 + Math.random() * 0.3;
714+
this.spinStartTime = Date.now();
715+
this.startRotation = this.rotation;
716+
const minRotations = this.spinDuration <= 2 ? 3 : this.spinDuration <= 4 ? 5 : 8;
717+
const maxRotations = this.spinDuration <= 2 ? 5 : this.spinDuration <= 4 ? 8 : 12;
718+
const totalRotations = minRotations + Math.random() * (maxRotations - minRotations);
719+
const randomOffset = Math.random() * 2 * Math.PI;
720+
this.targetRotation = this.startRotation + totalRotations * 2 * Math.PI + randomOffset;
630721
const spinBtn = document.getElementById('spinBtn');
631722
spinBtn.disabled = true;
632723
spinBtn.textContent = '🎲 Spinning...';
633724
this.canvas.classList.add('spinning');
634-
if (this.soundEnabled) this.playSpinSound();
725+
if (this.soundEnabled) this.sound.spin(this.spinDuration * 1000, this.spinDuration <= 2 ? 40 : this.spinDuration <= 4 ? 30 : 25);
635726
this.animate()
636727
}
637728

729+
easeOut(t) {
730+
return 1 - Math.pow(1 - t, 3)
731+
}
732+
638733
animate() {
639-
if (this.spinning) {
640-
this.rotation += this.spinSpeed;
641-
this.spinSpeed *= this.friction;
642-
if (this.spinSpeed < this.minSpeed) {
643-
this.spinning = false;
644-
this.spinSpeed = 0;
645-
const spinBtn = document.getElementById('spinBtn');
646-
spinBtn.disabled = false;
647-
spinBtn.textContent = '🎲 Spin the Wheel';
648-
this.canvas.classList.remove('spinning');
649-
this.showResult()
650-
}
734+
if (!this.spinning) return;
735+
const elapsed = (Date.now() - this.spinStartTime) / 1000;
736+
const progress = Math.min(elapsed / this.spinDuration, 1);
737+
if (progress >= 1) {
738+
this.spinning = false;
739+
this.rotation = this.targetRotation;
740+
const spinBtn = document.getElementById('spinBtn');
741+
spinBtn.disabled = false;
742+
spinBtn.textContent = '🎲 Spin the Wheel';
743+
this.canvas.classList.remove('spinning');
744+
this.sound.stop();
745+
this.showResult()
746+
} else {
747+
const easedProgress = this.easeOut(progress);
748+
this.rotation = this.startRotation + (this.targetRotation - this.startRotation) * easedProgress;
651749
this.draw();
652-
if (this.spinning) requestAnimationFrame(() => this.animate())
750+
requestAnimationFrame(() => this.animate())
653751
}
654752
}
655753

@@ -666,7 +764,6 @@ <h4>Recent Results</h4>
666764
if (this.history.length > 20) this.history = this.history.slice(0, 20);
667765
this.updateHistory();
668766
this.saveToStorage();
669-
if (this.soundEnabled) setTimeout(() => this.playWinSound(), 200);
670767
if (this.confettiEnabled) setTimeout(() => this.showConfetti(), 300)
671768
}
672769

@@ -723,42 +820,6 @@ <h4>Recent Results</h4>
723820
this.ctx.stroke()
724821
}
725822

726-
playSpinSound() {
727-
try {
728-
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
729-
const oscillator = audioContext.createOscillator();
730-
const gainNode = audioContext.createGain();
731-
oscillator.connect(gainNode);
732-
gainNode.connect(audioContext.destination);
733-
oscillator.frequency.setValueAtTime(150, audioContext.currentTime);
734-
oscillator.frequency.exponentialRampToValueAtTime(80, audioContext.currentTime + 0.8);
735-
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
736-
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.8);
737-
oscillator.start(audioContext.currentTime);
738-
oscillator.stop(audioContext.currentTime + 0.8)
739-
} catch (e) {
740-
}
741-
}
742-
743-
playWinSound() {
744-
try {
745-
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
746-
const notes = [523.25, 659.25, 783.99];
747-
notes.forEach((freq, i) => {
748-
const oscillator = audioContext.createOscillator();
749-
const gainNode = audioContext.createGain();
750-
oscillator.connect(gainNode);
751-
gainNode.connect(audioContext.destination);
752-
oscillator.frequency.setValueAtTime(freq, audioContext.currentTime + i * 0.1);
753-
gainNode.gain.setValueAtTime(0.15, audioContext.currentTime + i * 0.1);
754-
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + i * 0.1 + 0.3);
755-
oscillator.start(audioContext.currentTime + i * 0.1);
756-
oscillator.stop(audioContext.currentTime + i * 0.1 + 0.3)
757-
})
758-
} catch (e) {
759-
}
760-
}
761-
762823
showConfetti() {
763824
const colors = ['#3182ce', '#38a169', '#e53e3e', '#d69e2e', '#805ad5', '#dd6b20'];
764825
for (let i = 0; i < 30; i++) {
@@ -779,7 +840,9 @@ <h4>Recent Results</h4>
779840

780841
saveToStorage() {
781842
localStorage.setItem('wheelSegments', JSON.stringify(this.segments));
782-
localStorage.setItem('wheelHistory', JSON.stringify(this.history))
843+
localStorage.setItem('wheelHistory', JSON.stringify(this.history));
844+
localStorage.setItem('spinDuration', this.spinDuration);
845+
localStorage.setItem('soundEnabled', this.soundEnabled)
783846
}
784847

785848
reset() {

0 commit comments

Comments
 (0)