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 ;
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 ;
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