Le pitch
SpinEat est une roulette interactive pour résoudre l’éternel dilemme du déjeuner : “On mange quoi ?”. On fait tourner la roue, la physique fait le reste. Le résultat tombe, et un lien Google Maps permet de trouver un restaurant à proximité.
Essayer : presque.cool/spin-eat/
Pourquoi ce projet
Le midi au bureau, c’est toujours la même scène : personne ne sait quoi manger, personne ne veut décider, et la discussion tourne en rond. L’idée d’une app est née en rigolant — puis mon boss m’a mis au défi de la faire.
Je voulais montrer qu’on pouvait produire un prototype fonctionnel rapidement. Un jour plus tard, SpinEat était en production.
La roue : pourquoi Canvas
Le choix de Canvas plutôt que CSS ou SVG n’est pas anodin. La roue tourne avec une physique de friction — elle ralentit progressivement à chaque frame. Ce type d’animation continue nécessite de redessiner l’intégralité de la roue à 60 fps.
Avantages de Canvas pour ce cas d’usage :
- Animation fluide —
requestAnimationFrame+ redraw complet à chaque frame. Canvas est optimisé pour ce rendu intensif - Calcul de position simplifié — la position exacte sous la flèche se calcule via l’angle de rotation et la taille des segments. En CSS/SVG, il faudrait parser les transformations matricielles
- Emojis dynamiques — chaque emoji est positionné et orienté selon l’angle de son segment. En SVG, cela nécessiterait de manipuler des attributs
transformsur chaque élément à chaque frame - Performance — redessiner 10 segments avec leurs emojis 60 fois par seconde est plus efficace en Canvas qu’en manipulant le DOM
Inconvénients acceptés : pas d’éléments DOM inspectables (accessibilité limitée), pas de CSS styling direct, pixelisation possible sur écrans haute densité (gérée via devicePixelRatio).
La physique de friction
L’animation repose sur un modèle de friction simple mais efficace, calibré à l’instinct :
velocity *= 0.988 // Friction par frame
rotation += velocity // Mise à jour de la position
if (velocity < 0.001) stop()
La vitesse initiale est aléatoire (entre 0.3 et 0.7 radians/frame), ce qui produit des durées de rotation naturellement variées. La décélération non-linéaire (multiplication plutôt qu’addition) donne un ralentissement progressif très satisfaisant — la roue hésite avant de s’arrêter.
Le calcul du gagnant est purement géométrique : on normalise l’angle de rotation par rapport à la position de la flèche (en haut, à 270°), puis on divise par l’angle de chaque segment.
Les options de cuisine
Les 10 options par défaut couvrent les types de restauration que l’on croise le plus souvent en ville : Sushi, Burger, Pizza, Tacos, Tikka Masala, Poke/Salade, Pho, Kebab, Dim Sum, Brasserie. Chaque option a un emoji, un nom, une description et une couleur de segment personnalisable.
Les utilisateurs peuvent modifier, ajouter ou supprimer des options. Le sélecteur d’emoji propose 143 emojis food — l’ensemble de la catégorie “food & drink” d’Unicode. Les préférences sont persistées en localStorage.
Le sous-titre tournant
Chaque chargement de page affiche un sous-titre différent parmi cinq expressions françaises liées à la gastronomie. Un petit détail de personnalité qui change l’ambiance à chaque visite.
Le confetti
Ajouté après la première version fonctionnelle. Quand le résultat tombe, 150 particules de confetti éclatent à l’écran via canvas-confetti. Un détail superflu mais qui rend le moment de la révélation plus festif. Le bouton “Trouver un restaurant” lance ensuite une recherche Google Maps avec le type de cuisine sélectionné.
Stack technique
- React + TypeScript — Composants fonctionnels, typage strict
- Vite + SWC — Build rapide
- Canvas API — Rendu de la roue, DPI-aware (devicePixelRatio)
- Tailwind CSS — Couleurs OKLch, dark mode, animations custom
- Zustand — State management, persistance localStorage
- canvas-confetti — Animation de célébration
- PWA — installable, offline, Workbox
- i18n maison — Français/anglais, détection automatique du navigateur
- Compression — Brotli + Gzip sur les assets
- CSP — Content-Security-Policy avec hashes auto-générés
Évolutions
| Date | Version | Description |
|---|---|---|
| Fév 2026 | 1.0 | Version initiale — Roue Canvas, physique, options custom |
Bilan
Durée : 1 jour (février 2026).
Ce que j’ai appris :
- Que Canvas est le bon outil quand on a besoin d’animation physique continue — le duo
requestAnimationFrame+ friction rend la roue vivante - Que Zustand rend le state management trivial — persistance localStorage, slices séparés, zéro boilerplate
Ce que je referais pareil :
- Répondre à un défi avec un prototype livré en une journée — c’est la meilleure démonstration
- L’animation de friction calibrée à l’instinct — elle sonne juste, pas besoin de modèle physique complexe
Ce que je changerais :
- Explorer le retour haptique sur mobile pour accompagner le passage de chaque segment
- Ajouter un mode “tournoi” pour éliminer les options une par une jusqu’au choix final
The pitch
SpinEat is an interactive roulette to solve the eternal lunch dilemma: “What are we eating?”. Spin the wheel, physics does the rest. The result lands, and a Google Maps link helps find a nearby restaurant.
Try it: presque.cool/spin-eat/
Why this project
Lunchtime at the office is always the same scene: nobody knows what to eat, nobody wants to decide, and the discussion goes in circles. The idea for an app came up as a joke — then my boss challenged me to actually build it.
I wanted to show that a functional prototype could be shipped quickly. One day later, SpinEat was in production.
The wheel: why Canvas
Choosing Canvas over CSS or SVG wasn’t arbitrary. The wheel spins with friction physics — it gradually decelerates on every frame. This type of continuous animation requires redrawing the entire wheel at 60 fps.
Canvas advantages for this use case:
- Smooth animation —
requestAnimationFrame+ full redraw each frame. Canvas is optimized for this type of intensive rendering - Simplified position calculation — the exact position under the arrow is computed via rotation angle and segment size. With CSS/SVG, you’d need to parse matrix transformations
- Dynamic emojis — each emoji is positioned and rotated according to its segment angle. In SVG, this would require manipulating
transformattributes on each element every frame - Performance — redrawing 10 segments with their emojis 60 times per second is more efficient in Canvas than manipulating the DOM
Accepted trade-offs: no inspectable DOM elements (limited accessibility), no direct CSS styling, potential pixelation on high-density screens (handled via devicePixelRatio).
Friction physics
The animation relies on a simple but effective friction model, calibrated by feel:
velocity *= 0.988 // Friction per frame
rotation += velocity // Update position
if (velocity < 0.001) stop()
The initial velocity is random (between 0.3 and 0.7 radians/frame), producing naturally varied spin durations. Non-linear deceleration (multiplication rather than subtraction) creates a progressive slowdown that feels satisfying — the wheel hesitates before stopping.
The winner calculation is purely geometric: normalize the rotation angle relative to the arrow position (top, at 270°), then divide by each segment’s angle.
Food options
The 10 default options cover the most common restaurant types found in a city: Sushi, Burger, Pizza, Tacos, Tikka Masala, Poke/Salad, Pho, Kebab, Dim Sum, Brasserie. Each option has an emoji, a name, a description, and a customizable segment color.
Users can modify, add, or delete options. The emoji picker offers 143 food emojis — the full Unicode “food & drink” category. Preferences are persisted in localStorage.
The rotating subtitle
Each page load displays a different subtitle from five French food-related expressions. A small personality detail that changes the vibe on every visit.
The confetti
Added after the first working version. When the result lands, 150 confetti particles burst on screen via canvas-confetti. A superfluous detail that makes the reveal moment more festive. The “Find a restaurant” button then launches a Google Maps search with the selected cuisine type.
Tech stack
- React + TypeScript — Functional components, strict typing
- Vite + SWC — Fast builds
- Canvas API — Wheel rendering, DPI-aware (devicePixelRatio)
- Tailwind CSS — OKLch colors, dark mode, custom animations
- Zustand — State management, localStorage persistence
- canvas-confetti — Celebration animation
- PWA — installable, offline, Workbox
- i18n custom — French/English, automatic browser detection
- Compression — Brotli + Gzip on assets
- CSP — Content-Security-Policy with auto-generated hashes
Timeline
| Date | Version | Description |
|---|---|---|
| Feb 2026 | 1.0 | Initial version — Canvas wheel, physics, custom options |
Takeaways
Duration: 1 day (February 2026).
What I learned:
- That Canvas is the right tool when you need continuous physics animation — the
requestAnimationFrame+ friction duo makes the wheel feel alive - That Zustand makes state management trivial — localStorage persistence, separate slices, zero boilerplate
What I’d do the same:
- Answering a challenge with a prototype shipped in one day — it’s the best demonstration
- The friction animation tuned by feel — it sounds right, no need for a complex physics model
What I’d change:
- Explore haptic feedback on mobile to accompany each segment passing
- Add a “tournament” mode to eliminate options one by one until the final choice