--- title: "Building a 60 FPS Interactive Wheel of Fortune in HTML5 Canvas & GSAP: Friction, Physics & Sound Triggering" description: "How to build a high-performance 60 FPS wheel of fortune using HTML5 2D Canvas, GSAP custom easing, physics damping, and Web Audio tick synchronization." tags: ["javascript", "webdev", "canvas", "frontend"] canonical_url: "https://entscheidomat.com/ratgeber/gluecksrad-online-drehen" target_keywords: ["glücksrad online", "glücksrad drehen", "entscheidungsrad online", "canvas wheel of fortune", "gsap wheel animation"] --- # Building a 60 FPS Interactive Wheel of Fortune in HTML5 Canvas & GSAP: Friction, Physics & Sound Triggering Interactive spinners and wheels of fortune are among the most engaging UI components on the web. From marketing giveaways to decision utilities like a [Glücksrad online](https://entscheidomat.com/gluecksrad), a well-designed wheel needs to look smooth, feel physically grounded, and land accurately on its selected segment without visual stuttering. However, naive implementations using CSS rotations or DOM elements (`
` slices rotated around a pivot) quickly suffer from performance degradation, text blurriness, and dynamic segment layout bugs when scaling beyond 8–10 items. In this article, we will build a production-ready, 60 FPS interactive **HTML5 2D Canvas Wheel of Fortune** integrated with **GSAP (GreenSock)**, custom physics deceleration, and real-time Web Audio tick sound effects. --- ## 1. The Physics of Rotational Friction & Segment Indexing To make a digital wheel feel tangible, its deceleration must mimic physical rotational friction. ### Rotational Physics Equations When a force spins a wheel, it acquires an initial angular velocity $\omega_0$ (radians per second). Under constant angular friction $\alpha$, its angular displacement $\theta(t)$ over time $t$ is: $$\theta(t) = \omega_0 t - \frac{1}{2} \alpha t^2$$ In GSAP, we can model this friction curve smoothly using `power4.out` or `cubic-bezier(0.25, 1, 0.5, 1)` easing. ### Calculating Segment Index from Final Angle Suppose a wheel has $N$ segments, each occupying an arc angle of $\Delta \theta = \frac{2\pi}{N}$ radians ($360^\circ / N$). If the wheel settles at a total cumulative rotation angle $\theta_{\text{total}}$ (in degrees), and the pointer is located at the top ($270^\circ$ or $90^\circ$ offset depending on canvas coordinate space), the winning segment index $I_{\text{win}}$ is calculated as: $$I_{\text{win}} = \left\lfloor \frac{(360 - (\theta_{\text{total}} \bmod 360) + \text{offset}) \bmod 360}{360 / N} \right\rfloor$$ --- ## 2. Drawing Responsive Canvas Arcs in TypeScript Below is the core HTML5 2D Canvas renderer. It handles dynamic segment counts, vibrant color palettes, crisp text rendering, and high-DPI (Retina) display scaling. ```typescript export interface WheelSegment { label: string; color: string; } export class CanvasWheelRenderer { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private segments: WheelSegment[]; constructor(canvas: HTMLCanvasElement, segments: WheelSegment[]) { this.canvas = canvas; const context = canvas.getContext("2d"); if (!context) throw new Error("Could not get 2D context"); this.ctx = context; this.segments = segments; this.setupHighDPI(); } private setupHighDPI(): void { const dpr = window.devicePixelRatio || 1; const rect = this.canvas.getBoundingClientRect(); this.canvas.width = rect.width * dpr; this.canvas.height = rect.height * dpr; this.ctx.scale(dpr, dpr); } /** * Renders the wheel at a given rotation angle (in degrees). */ public draw(rotationAngleDeg: number): void { const rect = this.canvas.getBoundingClientRect(); const width = rect.width; const height = rect.height; const centerX = width / 2; const centerY = height / 2; const radius = Math.min(centerX, centerY) - 10; const numSegments = this.segments.length; const arcAngle = (2 * Math.PI) / numSegments; const rotationRad = (rotationAngleDeg * Math.PI) / 180; this.ctx.clearRect(0, 0, width, height); this.ctx.save(); this.ctx.translate(centerX, centerY); this.ctx.rotate(rotationRad); // 1. Draw Segments for (let i = 0; i < numSegments; i++) { const startAngle = i * arcAngle; const endAngle = startAngle + arcAngle; this.ctx.beginPath(); this.ctx.moveTo(0, 0); this.ctx.arc(0, 0, radius, startAngle, endAngle); this.ctx.closePath(); this.ctx.fillStyle = this.segments[i].color; this.ctx.fill(); this.ctx.lineWidth = 2; this.ctx.strokeStyle = "#ffffff"; this.ctx.stroke(); // 2. Draw Text Labels this.ctx.save(); this.ctx.rotate(startAngle + arcAngle / 2); this.ctx.textAlign = "right"; this.ctx.fillStyle = "#ffffff"; this.ctx.font = "bold 14px sans-serif"; this.ctx.shadowColor = "rgba(0,0,0,0.5)"; this.ctx.shadowBlur = 4; this.ctx.fillText(this.segments[i].label, radius - 20, 5); this.ctx.restore(); } this.ctx.restore(); // 3. Draw Fixed Top Pointer (Indicator) this.drawPointer(centerX, centerY - radius); } private drawPointer(x: number, y: number): void { this.ctx.save(); this.ctx.beginPath(); this.ctx.moveTo(x - 12, y - 10); this.ctx.lineTo(x + 12, y - 10); this.ctx.lineTo(x, y + 15); this.ctx.closePath(); this.ctx.fillStyle = "#ef4444"; this.ctx.fill(); this.ctx.strokeStyle = "#ffffff"; this.ctx.lineWidth = 2; this.ctx.stroke(); this.ctx.restore(); } } ``` --- ## 3. Integrating GSAP Animation & Web Audio Ticks To achieve 60 FPS animation with dynamic audio feedback, we hook GSAP's `gsap.to()` tween to our Canvas renderer's `draw()` method. Every time the rotation crosses a segment boundary, we trigger a short Web Audio tick sound. ```typescript import gsap from "gsap"; export class InteractiveWheelController { private renderer: CanvasWheelRenderer; private currentRotation: number = 0; private numSegments: number; private lastTickSegment: number = -1; private audioCtx?: AudioContext; constructor(renderer: CanvasWheelRenderer, numSegments: number) { this.renderer = renderer; this.numSegments = numSegments; } private playTickSound(): void { if (!this.audioCtx) { this.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); } if (this.audioCtx.state === "suspended") { this.audioCtx.resume(); } const osc = this.audioCtx.createOscillator(); const gain = this.audioCtx.createGain(); osc.type = "triangle"; osc.frequency.setValueAtTime(600, this.audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(150, this.audioCtx.currentTime + 0.03); gain.gain.setValueAtTime(0.3, this.audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.03); osc.connect(gain); gain.connect(this.audioCtx.destination); osc.start(); osc.stop(this.audioCtx.currentTime + 0.03); } public spinToSegment(winningIndex: number, onComplete?: () => void): void { const segmentAngle = 360 / this.numSegments; // Target angle calculation: Full spins (5 rotations) + segment offset const fullSpins = 5 * 360; // Align winning segment to top pointer (270 degrees) const targetSegmentOffset = 270 - (winningIndex * segmentAngle + segmentAngle / 2); // Normalize target angle const targetRotation = this.currentRotation + fullSpins + (targetSegmentOffset - (this.currentRotation % 360)); gsap.to(this, { currentRotation: targetRotation, duration: 4.5, ease: "power4.out", onUpdate: () => { // Redraw Canvas this.renderer.draw(this.currentRotation); // Calculate tick boundaries const currentSegment = Math.floor((this.currentRotation % 360) / segmentAngle); if (currentSegment !== this.lastTickSegment) { this.playTickSound(); this.lastTickSegment = currentSegment; } }, onComplete: () => { if (onComplete) onComplete(); } }); } } ``` --- ## 4. Performance Optimization Checklist | Optimization | Method | Impact | | :--- | :--- | :--- | | **High-DPI Retina Displays** | `canvas.width = width * devicePixelRatio` | Prevents blurry text on iPhones/Macs | | **Procedural Audio** | Web Audio Oscillators instead of MP3 files | 0ms audio latency, zero network requests | | **Single Canvas Pipeline** | Direct 2D context drawing over DOM elements | Constant 60 FPS performance regardless of segment count | | **GSAP Power4.out** | Realistic friction deceleration curve | Natural physical wheel feel | --- ## Summary & Live Demo 1. HTML5 Canvas 2D is significantly faster and cleaner than rotating DOM elements for wheels with dynamic segment counts. 2. Combine **GSAP `power4.out` easing** with **Web Audio API procedural sound ticks** for maximum user delight. 3. Calculate winning segment indices mathematically beforehand to guarantee deterministic UI outcomes. Test an interactive decision wheel live on [Entscheidomat Glücksrad Online](https://entscheidomat.com/gluecksrad). --- ## FAQ (Schema Structured Data) ```json { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "Why use HTML5 Canvas instead of CSS for a Wheel of Fortune?", "acceptedAnswer": { "@type": "Answer", "text": "HTML5 Canvas renders all segments and text in a single 60 FPS draw call, eliminating DOM bloat, layout shifts, and blurry text rendering on high-DPI displays." } }, { "@type": "Question", "name": "How do you trigger tick sound effects on a digital wheel?", "acceptedAnswer": { "@type": "Answer", "text": "By monitoring the rotation angle during animation updates and triggering a short Web Audio API oscillator burst whenever the angle crosses a segment boundary angle." } } ] } ```