Aller au contenu principal

Terre ASCII

Ouvrir le globe. Faites glisser la souris ou le doigt pour orienter librement la vue ; la rotation automatique s'interrompt pendant l'interaction et reprend ensuite progressivement. Le zoom et le panoramique sont désactivés pour ne pas perturber la navigation sur la page. Le même pipeline de rendu alimente l’aperçu de l’accueil et la page d’expérience dédiée.

Conversion de l'image en texte​

L'objet sous-jacent est une sphère 3D Three.js. Un ShaderMaterial personnalisé échantillonne deux textures distinctes : un masque spéculaire océanique et une carte de relief topographique :

// Séparation de la détection terrestre et du relief
// Le masque spéculaire est clair sur les océans ; son inversion les garde noirs
// La carte de relief module ensuite les terres sans éclairage dynamique supplémentaire
const material = new THREE.ShaderMaterial({
uniforms: {
oceanMap: {value: oceanTexture},
reliefMap: {value: reliefTexture},
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D oceanMap;
uniform sampler2D reliefMap;
varying vec2 vUv;
void main() {
float ocean = texture2D(oceanMap, vUv).g;
float land = 1.0 - smoothstep(0.28, 0.72, ocean);
float relief = texture2D(reliefMap, vUv).r;
float luminance = land * (0.48 + relief * 0.52);
gl_FragColor = vec4(vec3(luminance), 1.0);
}
`,
});

Le shader lit le canal vert de la carte spéculaire, le transforme en masque terrestre lissé, puis module la luminance des terres avec la carte de relief. Les océans restent sombres sans source lumineuse mobile.

AsciiEffect de Three.js effectue un rendu hors écran de cette sphère, sous-échantillonne les pixels et projette la luminance sur une série de glyphes textuels :

const effect = new AsciiEffect(renderer, ' .:-+*=%~@#01o', {
invert: true,
resolution: 0.16,
});

L'espace initial laisse les zones océaniques vides. Les continents se détachent ainsi directement sans disque sombre artificiel autour du globe. Lorsque la vue est dominée par un océan, comme au centre du Pacifique, les caractères deviennent rares : c’est la projection géométrique attendue, pas un blocage du programme.

La forme et la densité visuelle des caractères ont été ajustées. Les traits horizontaux de -, = et ~ produisent un léger effet de courbes de niveau sur les reliefs peu marqués.

Rotation et contrôles​

Le globe tourne autour de l’axe Y à une vitesse de 0,28 rad/s, avec une cadence limitée à environ 30 FPS pour réduire la consommation continue du CPU et du GPU :

const controls = new OrbitControls(camera, effect.domElement);
controls.enableZoom = false;
controls.enablePan = false;
controls.rotateSpeed = 0.6;
controls.enableDamping = true;
controls.dampingFactor = 0.06;

Le glissement modifie l’angle de la caméra via OrbitControls et interrompt la rotation automatique. Le bouton de pause arrête cette rotation, mais les changements de caméra redessinent toujours le globe ; l’amortissement du glissement se termine avant l’arrêt des demandes de trames inactives. L’animation se suspend hors champ ou dans un onglet masqué. Les changements de visibilité et la lecture réveillent le moteur lorsque les conditions le permettent.

Protection contre les tampons de taille nulle​

AsciiEffect calcule les dimensions du canevas d’échantillonnage avec Math.floor(width * resolution) et Math.floor(height * resolution). À une résolution de 0,16, une dimension du conteneur inférieure à 6,25 pixels donne zéro après cet arrondi. Appeler alors getImageData(0, 0, 0, 0) provoque une erreur fatale dans le navigateur. Le composant vérifie les dimensions avant le redimensionnement. Un conteneur trop petit remet hasSize à false : draw() ne dessine plus tant que les dimensions ne sont pas redevenues valides :

const resize = () => {
const {width, height} = mountNode.getBoundingClientRect();
const nextWidth = Math.round(width);
const nextHeight = Math.round(height);
if (disposed || nextWidth < 1 / ASCII_RESOLUTION || nextHeight < 1 / ASCII_RESOLUTION) {
hasSize = false;
return;
}
camera.aspect = nextWidth / nextHeight;
camera.updateProjectionMatrix();
effect.setSize(nextWidth, nextHeight);
hasSize = true;
draw();
};

Cycle de vie et nettoyage​

Au démontage, le composant libère toutes ses ressources et références :

  1. Il annule le requestAnimationFrame en attente.
  2. Il déconnecte ResizeObserver et IntersectionObserver.
  3. Il libère OrbitControls, les textures, la géométrie de la sphère, le matériau shader et le moteur WebGL, puis force la libération du contexte.
  4. Il retire le nœud de la grille de caractères du DOM.
Explorer les liensOuvrir le réseau