---
title: SpinEat
canonical: "https://presque.cool/projects/spin-eat/"
description: "« On mange quoi ? » — une roulette pour trancher l'éternel dilemme du déjeuner. La roue tourne, le hasard décide, Google Maps fait le reste."
---

<div data-lang="fr">

## 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/](https://presque.cool/spin-eat/)

<div class="quick-overview">
  <div class="overview-item">
    <span class="overview-label">Durée</span>
    <span class="overview-value">~1 jour</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Période</span>
    <span class="overview-value">Fév 2026</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Stack</span>
    <div class="tech-tags">
      <span class="tech-tag">React</span>
      <span class="tech-tag">TypeScript</span>
      <span class="tech-tag">Canvas</span>
      <span class="tech-tag">PWA</span>
    </div>
  </div>
  <div class="overview-item">
    <span class="overview-label">Statut</span>
    <span class="overview-value accent">✓ En production</span>
  </div>
</div>

---

## 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 `transform` sur 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 :

```text
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

<ul class="tech-stack">
  <li><strong>React</strong> + <strong>TypeScript</strong> — Composants fonctionnels, typage strict</li>
  <li><strong>Vite</strong> + <strong>SWC</strong> — Build rapide</li>
  <li><strong>Canvas API</strong> — Rendu de la roue, DPI-aware (devicePixelRatio)</li>
  <li><strong>Tailwind CSS</strong> — Couleurs OKLch, dark mode, animations custom</li>
  <li><strong>Zustand</strong> — State management, persistance localStorage</li>
  <li><strong>canvas-confetti</strong> — Animation de célébration</li>
  <li><strong>PWA</strong> — installable, offline, Workbox</li>
  <li><strong>i18n</strong> maison — Français/anglais, détection automatique du navigateur</li>
<li><strong>Compression</strong> — Brotli + Gzip sur les assets</li>
<li><strong>CSP</strong> — Content-Security-Policy avec hashes auto-générés</li>
</ul>

---

## É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

</div>

<div data-lang="en">

## 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/](https://presque.cool/spin-eat/)

<div class="quick-overview">
  <div class="overview-item">
    <span class="overview-label">Duration</span>
    <span class="overview-value">~1 day</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Period</span>
    <span class="overview-value">Feb 2026</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Stack</span>
    <div class="tech-tags">
      <span class="tech-tag">React</span>
      <span class="tech-tag">TypeScript</span>
      <span class="tech-tag">Canvas</span>
      <span class="tech-tag">PWA</span>
    </div>
  </div>
  <div class="overview-item">
    <span class="overview-label">Status</span>
    <span class="overview-value accent">✓ In production</span>
  </div>
</div>

---

## 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 `transform` attributes 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:

```text
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

<ul class="tech-stack">
  <li><strong>React</strong> + <strong>TypeScript</strong> — Functional components, strict typing</li>
  <li><strong>Vite</strong> + <strong>SWC</strong> — Fast builds</li>
  <li><strong>Canvas API</strong> — Wheel rendering, DPI-aware (devicePixelRatio)</li>
  <li><strong>Tailwind CSS</strong> — OKLch colors, dark mode, custom animations</li>
  <li><strong>Zustand</strong> — State management, localStorage persistence</li>
  <li><strong>canvas-confetti</strong> — Celebration animation</li>
  <li><strong>PWA</strong> — installable, offline, Workbox</li>
  <li><strong>i18n</strong> custom — French/English, automatic browser detection</li>
<li><strong>Compression</strong> — Brotli + Gzip on assets</li>
<li><strong>CSP</strong> — Content-Security-Policy with auto-generated hashes</li>
</ul>

---

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

</div>
