Vue normale

Reçu avant avant-hier

Un développeur fait tourner Doom dans un navigateur, avec du CSS et rien d'autre

Par :Korben
31 mars 2026 à 06:24

Niels Leenheer a porté le mythique Doom de 1993 dans un navigateur web, mais sans WebGL ni Canvas. Tout le rendu 3D repose sur des div et des transformations CSS. Le résultat est jouable, open source, et un brin absurde. On adore.

Doom en div

Le principe est aussi fou qu'il en a l'air. Chaque mur, chaque sol, chaque tonneau et chaque ennemi est une balise div, positionnée dans l'espace 3D grâce aux transformations CSS. Le jeu lit les données du fichier WAD original de 1993, celui-là même qui contenait les niveaux du Doom d'id Software, et en extrait les coordonnées des murs, des secteurs et des textures.

La logique du jeu, elle, tourne en JavaScript. Mais côté affichage, c'est 100 % CSS : pas de Canvas, pas de WebGL, pas de bibliothèque graphique. Juste des div, des calculs trigonométriques en CSS et des propriétés personnalisées.

Pour simuler une caméra, le développeur a trouvé une astuce assez maline : plutôt que de déplacer le joueur dans la scène, c'est la scène entière qui bouge dans le sens inverse. Le CSS ne gère pas nativement la notion de caméra, du coup Leenheer a tout simplement inversé le problème.

Des fonctions CSS qu'on ne soupçonnait pas

Le projet exploite des fonctions CSS relativement récentes : hypot() pour le théorème de Pythagore, atan2() pour les angles de rotation, clip-path pour découper les sols en polygones complexes, et @property pour animer des propriétés personnalisées qui servent à gérer les portes, les ascenseurs et même la chute du joueur.

Les ennemis utilisent des spritesheets classiques avec un effet de billboard, c'est-à-dire qu'ils font toujours face à la caméra. Les effets de lumière passent par un filtre brightness sur chaque secteur, et le fameux ennemi invisible Spectre utilise un filtre SVG pour reproduire l'effet de distorsion du jeu original.

Leenheer a même ajouté un mode spectateur avec caméra libre, absent du Doom de 1993, et les calculs de positionnement de cette caméra reposent eux aussi sur les fonctions trigonométriques du CSS.

Les limites du CSS poussé à fond

Le jeu est jouable sur cssdoom.wtf, et le code source est disponible sur GitHub sous licence GPL 2. Par contre, les performances restent limitées. Sur Safari iOS, le jeu peut planter au bout de quelques minutes, et les gros niveaux font souffrir le navigateur.

Leenheer le reconnaît lui-même : le projet ne remplacera jamais WebGL ou WebGPU pour du rendu 3D sérieux. Le but était avant tout de montrer jusqu'où le CSS moderne peut aller, et sur ce point, la démonstration est plutôt convaincante.

C'est le genre de projet complètement absurde qui force le respect. Doom a déjà été porté sur à peu près tout ce qui contient un processeur, des calculatrices aux tests de grossesse, et voilà qu'il tourne maintenant dans une feuille de style.

L'air de rien, ça montre que le CSS de 2026 n'a plus grand-chose à voir avec celui qu'on utilisait pour centrer un div il y a dix ans.

Source : Huckster.io

Sortie de µJS, une bibliothèque JavaScript légère pour dynamiser un site sans framework

µJS est une bibliothèque JavaScript open source (licence MIT) qui permet de rendre un site web dynamique sans recourir à un framework frontend lourd. Elle s’inspire de pjax, Turbo et HTMX, avec pour objectif d’être plus simple et plus légère.

Principe de fonctionnement

µJS intercepte les clics sur les liens et les soumissions de formulaires pour charger les pages via AJAX, au lieu de déclencher un rechargement complet du navigateur. Le contenu récupéré remplace tout ou partie de la page courante. Le résultat : une navigation fluide, sans rechargement visible, sans écrire une seule ligne de JavaScript.

Aucune étape de build, aucune dépendance, compatible avec n’importe quel backend (PHP, Python, Go, Ruby…).

Fonctionnalités principales

  • Mode patch : mettre à jour plusieurs fragments du DOM en une seule requête, via des attributs mu-patch-target dans la réponse HTML du serveur
  • SSE : mises à jour en temps réel via Server-Sent Events
  • DOM morphing : préservation de l’état du DOM (focus, scroll, transitions CSS) via idiomorph
  • View Transitions : animations fluides entre les états de page, via l’API native du navigateur
  • Prefetch : préchargement de la page cible au survol d’un lien
  • Polling : rafraîchissement automatique d’un fragment à intervalle régulier
  • Verbes HTTP complets : GET, POST, PUT, PATCH, DELETE sur n’importe quel élément
  • Barre de progression : intégrée, sans dépendance externe

Installation

Via CDN :

<script src="https://cdn.jsdelivr.net/npm/@digicreon/mujs/dist/mu.min.js"></script>
<script>mu.init();</script>

Via npm :

npm install @digicreon/mujs

Exemple 1 : navigation AJAX sans configuration

Par défaut, tous les liens internes sont interceptés automatiquement. Le <body> de la page cible remplace le <body> courant.

<!DOCTYPE html>
<html>
<head>
    <title>Mon site</title>
</head>
<body>
    <nav>
        <a href="/">Accueil</a>
        <a href="/articles">Articles</a>
        <a href="/contact">Contact</a>
    </nav>

    <main id="contenu">
        <p>Contenu de la page.</p>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/@digicreon/mujs/dist/mu.min.js"></script>
    <script>mu.init();</script>
</body>
</html>

Aucun attribut supplémentaire. Les boutons retour/avant du navigateur fonctionnent, l’URL est mise à jour, le titre de la page aussi.

Pour ne remplacer qu’un fragment de la page plutôt que le <body> entier :

<a href="/articles" mu-target="#contenu" mu-source="#contenu">Articles</a>

Dans ce cas, µJS va récupérer la page /articles, va extraire l’élément #contenu de la réponse, et remplace l’élément #contenu courant avec.

Si tous les changements de pages se font dans l’élément #contenu, on peut généraliser dans la configuration (pour éviter d’avoir à mettre des attributs mu-target et mu-source sur tous les liens) :

<script>
mu.init({
    target: "#contenu",
    source: "#contenu"
});
</script>

Exemple 2 : recherche en direct avec debounce

<input type="text" name="q"
       mu-trigger="change"
       mu-debounce="300"
       mu-url="/recherche"
       mu-target="#resultats"
       mu-source="#resultats"
       mu-mode="update">

<div id="resultats"></div>

Le serveur reçoit une requête GET vers /recherche?q=... et retourne un fragment HTML. µJS l'injecte dans #resultats. Aucun JavaScript à écrire côté client.

Exemple 3 : mise à jour de plusieurs fragments en une seule requête (patch mode)

Côté HTML :

<form action="/commentaire/ajouter" method="post" mu-mode="patch">
    <textarea name="contenu"></textarea>
    <button type="submit">Envoyer</button>
</form>

<ul id="commentaires">
    <!-- liste des commentaires -->
</ul>

<span id="compteur">3 commentaires</span>

Le serveur retourne plusieurs fragments HTML dans une seule réponse. Chaque fragment indique sa cible via mu-patch-target :

<!-- Ajoute le nouveau commentaire à la liste -->
<li class="commentaire" mu-patch-target="#commentaires" mu-patch-mode="append">
    <p>Le nouveau commentaire</p>
</li>

<!-- Met à jour le compteur -->
<span mu-patch-target="#compteur">4 commentaires</span>

<!-- Réinitialise le formulaire -->
<form action="/commentaire/ajouter" method="post" mu-patch-target="form">
    <textarea name="contenu"></textarea>
    <button type="submit">Envoyer</button>
</form>

Une seule requête HTTP, trois fragments mis à jour simultanément. Le serveur garde le contrôle total sur ce qui est mis à jour et comment.

Commentaires : voir le flux Atom ouvrir dans le navigateur

❌