Up-in-the-Air – commitdiff

You can use Git to clone the repository via the web URL. Download snapshot (zip)
Collectibles adjust to graphics and high contrast settings
authorJulian Fietkau <git@fietkau.software>
Tue, 24 Sep 2024 23:13:01 +0000 (01:13 +0200)
committerJulian Fietkau <git@fietkau.software>
Tue, 24 Sep 2024 23:13:01 +0000 (01:13 +0200)
main.js

diff --git a/main.js b/main.js
index fadccfe68c6d16b1bb05cd0582748b76146f6855..3674f0f8c27c4a60d652e296e1977d8ee51bf489 100644 (file)
--- a/main.js
+++ b/main.js
@@ -4,12 +4,8 @@ import * as THREE from 'three';
 import { FontLoader } from 'three/addons/loaders/FontLoader.js';
 import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
 
-function getRandomWord() {
-  let words = [
-    "Alpaca", "Bat", "Butterfly", "Deer", "Duck", "Elk", "Giraffe", "Horse",
-    "Lynx", "Llama", "Owl", "Ocelot", "Pig", "Sheep", "Tapir", "Walrus",
-  ];
-  return words[Math.floor(Math.random() * words.length)];
+function getRandomWord(game) {
+  return game.assets.words[Math.floor(Math.random() * game.assets.words.length)];
 }
 
 function easeInOut(val) {
@@ -22,6 +18,10 @@ function lerp(start, end, progress) {
 
 function loadAllAssets(game, renderProgressCallback) {
   game.assets = {};
+  game.assets.words = [
+    "Alpaca", "Bat", "Butterfly", "Deer", "Duck", "Elk", "Giraffe", "Horse",
+    "Lynx", "Llama", "Owl", "Ocelot", "Pig", "Sheep", "Tapir", "Walrus",
+  ];
   return new Promise((resolve, reject) => {
     let todoList = {
       'audio/music.ogg': 1636930,
@@ -226,6 +226,31 @@ function init(game, canvas) {
   renderer.setAnimationLoop(() => { animate(game, renderer, scene); });
 }
 
+function prepareWordMesh(game, word) {
+  while(word.children.length > 0) {
+    word.remove(word.children[0]);
+  }
+  if(game.settings['graphics'] <= 2 && !game.settings['highcontrast']) {
+    for(let letter of word.text) {
+      let geometry = game.assets.fonts.geometry[letter];
+      let material = game.view.materials.letter;
+      if(geometry.customMaterial) {
+        material = geometry.customMaterial;
+      }
+      let mesh = new THREE.Mesh(geometry, material);
+      // We wrap each letter in a surrounding group in order to move the center point
+      // from the corner of the letter to its center. This makes rotations easier.
+      let container = new THREE.Group();
+      mesh.position.set(-game.assets.fonts.geometry[letter].dx, -game.assets.fonts.geometry[letter].dy, 0);
+      container.add(mesh);
+      word.add(container);
+    }
+  } else if(game.settings['graphics'] == 3 || game.settings['highcontrast']) {
+    let mesh = new THREE.Mesh(Object.values(game.assets.fonts.geometry)[0], game.view.materials.letter);
+    word.add(mesh);
+  }
+}
+
 function reset(game) {
   game.ui.reachedEnd = false;
   if(game.settings['graphics'] <= 2 && !game.settings['highcontrast']) {
@@ -249,13 +274,6 @@ function reset(game) {
   }
   game.objects.words = [];
   game.objects.words.collectedCount = 0;
-  game.assets.fonts.geometry = {};
-  let letterMaterial = new THREE.MeshStandardMaterial({
-    color: 0xffffff,
-    emissive: 0x606060,
-    roughness: 0.4,
-    metalness: 1,
-  });
   const interWordDistance = new THREE.Vector3();
   let placementSuccess;
   for(let i = 0; i < 100; i++) {
@@ -271,28 +289,8 @@ function reset(game) {
     let randomCameraX = game.courseRadius * Math.sin(angleInCourse * 2 * Math.PI);
     let randomCameraY = game.courseRadius * -Math.cos(angleInCourse * 2 * Math.PI);
     let word = new THREE.Group();
-    word.text = getRandomWord();
-    for(let letter of word.text) {
-      if(!(letter in game.assets.fonts.geometry)) {
-        game.assets.fonts.geometry[letter] = new TextGeometry(letter, {
-          font: game.assets.fonts.cookie,
-          size: 0.2,
-          depth: 0.03,
-          curveSegments: 2,
-          bevelEnabled: false,
-        });
-        game.assets.fonts.geometry[letter].computeBoundingBox();
-        let bbox = game.assets.fonts.geometry[letter].boundingBox;
-        // Add these to local 0,0 later to get the letter's center rotation point
-        game.assets.fonts.geometry[letter].dx = (bbox.max.x - bbox.min.x) / 2;
-        game.assets.fonts.geometry[letter].dy = (bbox.max.y - bbox.min.y) / 2;
-      }
-      let mesh = new THREE.Mesh(game.assets.fonts.geometry[letter], letterMaterial);
-      let container = new THREE.Group();
-      mesh.position.set(-game.assets.fonts.geometry[letter].dx, -game.assets.fonts.geometry[letter].dy, 0);
-      container.add(mesh);
-      word.add(container);
-    }
+    word.text = getRandomWord(game);
+    prepareWordMesh(game, word);
     word.randomAnimOffset = Math.random();
     const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
     let attempts = 0;
@@ -384,7 +382,6 @@ function animate(game, renderer, scene) {
         let collectionAnimationDuration = 1.0;
         for(let j = 0; j < word.children.length; j++) {
           let letter = word.children[j];
-          let wordProgress = j / word.children.length;
           let animationProgress = (((game.timeProgress + 5 * word.randomAnimOffset) % 5) / 5 + j / 37) % 1;
           x = game.objects.feather.scale.x * 0.5 * Math.cos(animationProgress * 1 * Math.PI * 2);
           y = 0.2 * Math.sin(animationProgress * 7 * Math.PI * 2);
@@ -512,7 +509,12 @@ function animate(game, renderer, scene) {
   game.objects.feather.twistSpeed = Math.min(0.13, game.objects.feather.twistSpeed) * game.settings['difficulty']['speed'] / 100;
   game.objects.feather.rotation.x = (game.objects.feather.rotation.x + game.objects.feather.twistSpeed) % (2 * Math.PI);
 
-  const collectedScale = lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
+  let collectedScale = 0.0;
+  if(!game.settings['highcontrast'] && game.settings['graphics'] <= 2) {
+    collectedScale = lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
+  } else if(!game.settings['highcontrast'] && game.settings['graphics'] == 3) {
+    collectedScale = 0.3;
+  }
   const collectingRadius = - 0.5 + 1.5 * game.settings['difficulty']['collectingradius'];
   for(let i = 0; i < game.objects.words.length; i++) {
     let word = game.objects.words[i];
@@ -522,30 +524,45 @@ function animate(game, renderer, scene) {
       word.collected = game.view.clock.getElapsedTime();
       game.objects.words.collectedCount += 1;
     }
+    if(word.parent != game.view.scene) {
+      // All that happens in here is the positional animation for the word, which
+      // we can skip if it is no longer visible.
+      continue;
+    }
     let x, y, z;
     let collectionAnimationDuration = 1.0;
     for(let j = 0; j < word.children.length; j++) {
       game.var.notCollectedPos.set(0, 0, 0);
       game.var.collectedPos.set(0, 0, 0);
       let letter = word.children[j];
-      let wordProgress = j / word.children.length;
       let animationProgress = (((game.timeProgress + 5 * word.randomAnimOffset) % 5) / 5 + j / 37) % 1;
       if(!word.collected || game.view.clock.getElapsedTime() - word.collected <= collectionAnimationDuration) {
-        const wordAnimationRadius = 0.2;
-        x = word.position.x + wordAnimationRadius * Math.cos(animationProgress * 5 * Math.PI * 2);
-        y = word.position.y + wordAnimationRadius * Math.sin(animationProgress * 4 * Math.PI * 2);
-        z = wordAnimationRadius * Math.sin(animationProgress * 6 * Math.PI * 2);
+        x = word.position.x;
+        y = word.position.y;
+        z = 0;
+        if(game.settings['graphics'] <= 2 && !game.settings['highcontrast']) {
+          const wordAnimationRadius = 0.2;
+          x += wordAnimationRadius * Math.cos(animationProgress * 5 * Math.PI * 2);
+          y += wordAnimationRadius * Math.sin(animationProgress * 4 * Math.PI * 2);
+          z += wordAnimationRadius * Math.sin(animationProgress * 6 * Math.PI * 2);
+        }
         game.var.notCollectedPos.set(x, y, z);
       }
       if(word.collected) {
-        x = game.objects.feather.scale.x * 0.5 * Math.cos(animationProgress * 1 * Math.PI * 2);
-        y = 0.2 * Math.sin(animationProgress * 7 * Math.PI * 2);
-        z = 0.2 * Math.cos(animationProgress * 7 * Math.PI * 2);
-        x = x * Math.cos(game.objects.feather.rotation.z) - y * Math.sin(game.objects.feather.rotation.z);
-        y = y * Math.cos(game.objects.feather.rotation.z) + x * Math.sin(game.objects.feather.rotation.z);
-        x += game.objects.feather.position.x;
-        y += game.objects.feather.position.y;
-        z += game.objects.feather.position.z;
+        if(!game.settings['highcontrast']) {
+          x = game.objects.feather.scale.x * 0.5 * Math.cos(animationProgress * 1 * Math.PI * 2);
+          y = 0.2 * Math.sin(animationProgress * 7 * Math.PI * 2);
+          z = 0.2 * Math.cos(animationProgress * 7 * Math.PI * 2);
+          x = x * Math.cos(game.objects.feather.rotation.z) - y * Math.sin(game.objects.feather.rotation.z);
+          y = y * Math.cos(game.objects.feather.rotation.z) + x * Math.sin(game.objects.feather.rotation.z);
+          x += game.objects.feather.position.x;
+          y += game.objects.feather.position.y;
+          z += game.objects.feather.position.z;
+        } else {
+          x = game.objects.feather.position.x;
+          y = game.objects.feather.position.y;
+          z = game.objects.feather.position.z;
+        }
         game.var.collectedPos.set(x, y, z);
       }
       if(game.var.notCollectedPos.length() > 0 && game.var.collectedPos.length() > 0) {
@@ -557,10 +574,16 @@ function animate(game, renderer, scene) {
         letter.position.set(game.var.notCollectedPos.x, game.var.notCollectedPos.y, game.var.notCollectedPos.z);
       } else if(game.var.collectedPos.length() > 0) {
         letter.position.set(game.var.collectedPos.x, game.var.collectedPos.y, game.var.collectedPos.z);
+        if(game.settings['highcontrast']) {
+          // Special case because in high contrast mode, collected words vanish entirely.
+          game.view.scene.remove(word);
+        }
       }
       letter.position.sub(word.position);
-      let rotation = (game.timeProgress * 3) % (2 * Math.PI);
-      letter.rotation.set(rotation + j, rotation + 2*j, rotation + 3*j);
+      if(!game.settings['highcontrast']) {
+        let rotation = (game.timeProgress * 3 + 2 * Math.PI * word.randomAnimOffset) % (2 * Math.PI);
+        letter.rotation.set(rotation + j, rotation + 2*j, rotation + 3*j);
+      }
     }
   }
 
@@ -711,8 +734,23 @@ function createMeshes(game) {
     game.objects.backdrop.material.dispose();
     game.objects.backdrop.geometry.dispose();
   }
-  game.view.materials = {};
+  if(game.assets.fonts.geometry) {
+    for(let geom of Object.values(game.assets.fonts.geometry)) {
+      if(geom.customMaterial) {
+        geom.customMaterial.dispose();
+      }
+      geom.dispose();
+    }
+    delete game.assets.fonts.geometry;
+  }
+  if(game.view.materials) {
+    for(let material of Object.values(game.view.materials)) {
+      material.dispose();
+    }
+    delete game.view.materials;
+  }
 
+  game.view.materials = {};
   if(!game.settings['highcontrast']) {
     let cloudShaders;
     let cloudGeometry = new THREE.PlaneGeometry(1, 200 / 350);
@@ -792,7 +830,57 @@ function createMeshes(game) {
         textureVariantSuffix = 'a';
       }
     }
+    game.view.materials.letter = new THREE.MeshStandardMaterial({
+      color: 0xffffff,
+      emissive: 0x606060,
+      roughness: 0.4,
+      metalness: 1,
+    });
     if(game.settings['graphics'] <= 2) {
+      game.assets.fonts.geometry = {};
+      for(let letter of [...new Set(game.assets.words.join(''))]) {
+        if(game.settings['graphics'] == 1) {
+          game.assets.fonts.geometry[letter] = new TextGeometry(letter, {
+            font: game.assets.fonts.cookie,
+            size: 0.2,
+            depth: 0.03,
+            curveSegments: 2,
+            bevelEnabled: false,
+          });
+          game.assets.fonts.geometry[letter].computeBoundingBox();
+          let bbox = game.assets.fonts.geometry[letter].boundingBox;
+          // Add these to local 0,0 later to get the letter's center rotation point
+          game.assets.fonts.geometry[letter].dx = (bbox.max.x - bbox.min.x) / 2;
+          game.assets.fonts.geometry[letter].dy = (bbox.max.y - bbox.min.y) / 2;
+        } else {
+          let letterCanvas = document.createElement('canvas');
+          letterCanvas.width = 64;
+          letterCanvas.height = 64;
+          let letterCanvasContext = letterCanvas.getContext('2d');
+          letterCanvasContext.font = '60px Cookie';
+          letterCanvasContext.fillStyle = '#000';
+          letterCanvasContext.fillRect(0, 0, letterCanvas.width, letterCanvas.height);
+          letterCanvasContext.fillStyle = '#fff';
+          let bbox = letterCanvasContext.measureText(letter);
+          let vOffset = bbox.actualBoundingBoxAscent - 0.5 * (bbox.actualBoundingBoxAscent + bbox.actualBoundingBoxDescent);
+          letterCanvasContext.fillText(letter, Math.round((letterCanvas.width - bbox.width) / 2), (letterCanvas.height / 2) + vOffset);
+          let alphaMap = new THREE.CanvasTexture(letterCanvas);
+          alphaMap.needsUpdate = true;
+          let letterMaterial = new THREE.MeshStandardMaterial({
+            color: game.view.materials.letter.color,
+            emissive: 0x303030,
+            roughness: game.view.materials.letter.roughness,
+            metalness: game.view.materials.letter.metalness,
+            side: THREE.DoubleSide,
+            alphaMap: alphaMap,
+            transparent: true,
+          });
+          game.assets.fonts.geometry[letter] = new THREE.PlaneGeometry(0.3, 0.3);
+          game.assets.fonts.geometry[letter].dx = 0;
+          game.assets.fonts.geometry[letter].dy = 0;
+          game.assets.fonts.geometry[letter].customMaterial = letterMaterial;
+        }
+      }
       let numClouds = 300;
       if(game.settings['graphics'] == 2) {
         numClouds = 75;
@@ -812,6 +900,13 @@ function createMeshes(game) {
         game.objects.clouds.add(cloud);
       }
     } else {
+      const minimalLetterGeometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
+      minimalLetterGeometry.dx = 0;
+      minimalLetterGeometry.dy = 0;
+      game.assets.fonts.geometry = {};
+      for(let letter of [...new Set(game.assets.words.join(''))]) {
+        game.assets.fonts.geometry[letter] = minimalLetterGeometry;
+      }
       for(let r = 0; r < game.courseRadius / 3; r++) {
         let angle = THREE.MathUtils.degToRad(360 * 3 * r / game.courseRadius);
         let cameraX = game.courseRadius * Math.sin(angle);
@@ -839,12 +934,28 @@ function createMeshes(game) {
     game.objects.backdrop = new THREE.Mesh(new THREE.PlaneGeometry(350, 350), game.view.materials['cloud0' + textureVariantSuffix]);
     game.objects.backdrop.position.setZ(-100);
   } else {
+    game.view.materials.letter = new THREE.MeshStandardMaterial({
+      color: 0x00ff00,
+      emissive: 0x00ff00,
+    });
+    const highcontrastLetterGeometry = new THREE.SphereGeometry(0.1, 16, 16);
+    highcontrastLetterGeometry.dx = 0;
+    highcontrastLetterGeometry.dy = 0;
+    game.assets.fonts.geometry = {};
+    for(let letter of [...new Set(game.assets.words.join(''))]) {
+      game.assets.fonts.geometry[letter] = highcontrastLetterGeometry;
+    }
     const highContrastBackdropMaterial = new THREE.MeshBasicMaterial({
       map: game.assets.textures['highcontrast-backdrop'],
     });
     game.objects.backdrop = new THREE.Mesh(new THREE.PlaneGeometry(150, 150), highContrastBackdropMaterial);
     game.objects.backdrop.position.setZ(-10);
   }
+  if(game.objects.words) {
+    for(let word of game.objects.words) {
+      prepareWordMesh(game, word);
+    }
+  }
   game.view.scene.add(game.objects.backdrop);
 }