9.9 KiB
title, description, tags, canonical_url, target_keywords
| title | description | tags | canonical_url | target_keywords | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Building a 60 FPS Interactive Wheel of Fortune in HTML5 Canvas & GSAP: Friction, Physics & Sound Triggering | 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. |
|
https://entscheidomat.com/ratgeber/gluecksrad-online-drehen |
|
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, 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 (<div> 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.
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.
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
- HTML5 Canvas 2D is significantly faster and cleaner than rotating DOM elements for wheels with dynamic segment counts.
- Combine GSAP
power4.outeasing with Web Audio API procedural sound ticks for maximum user delight. - Calculate winning segment indices mathematically beforehand to guarantee deterministic UI outcomes.
Test an interactive decision wheel live on Entscheidomat Glücksrad Online.
FAQ (Schema Structured Data)
{
"@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."
}
}
]
}