Up-in-the-Air – commitdiff

You can use Git to clone the repository via the web URL. Download snapshot (zip)
Add placeholder title screen
authorJulian Fietkau <git@fietkau.software>
Sun, 22 Sep 2024 00:32:37 +0000 (02:32 +0200)
committerJulian Fietkau <git@fietkau.software>
Sun, 22 Sep 2024 00:32:37 +0000 (02:32 +0200)
index.html
main.js

index f34369e8c6f02d8c5b33698985b103ef016de6d6..32e957f3755469558b18f329e507c509a07890eb 100644 (file)
@@ -31,6 +31,9 @@
     display: none;
     transition: opacity 250ms;
   }
+  .ui-page:not(.gameplay) {
+    z-index: 10;
+  }
   .ui-page.loading {
     display: flex;
     flex-direction: column;
     gap: 1ex;
     opacity: 1;
   }
+  .ui-page.title {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 1ex;
+  }
+  .ui-page.title .footer {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: end;
+    font-size: .7em;
+    padding: .7ex;
+  }
   canvas {
     box-sizing: border-box;
     display: block;
 <progress value="0" max="100"></progress>
 <div><span>0</span> %</div>
 </div>
+<div class="ui-page title">
+<h1>Up in the Air</h1>
+<div>
+<button class="startGame">Start game</button>
+</div>
+<div class="footer">
+<span>Version: dev</span>
+<span>A game for <a href="https://itch.io/jam/fedi-jam" target="_blank">FediJam 2024</a></span>
+<span>Website: <a href="https://fietkau.media/up_in_the_air" target="_blank">fietkau.media/up_in_the_air</a></span>
+</div>
+</div>
 <div class="ui-page gameplay">
 <canvas width="800" height="800"></canvas>
-<div class="options">
+<div class="options" style="display: none">
 <input type="checkbox" id="enableMusic"> <label for="enableMusic">Music</label>
 </div>
 </div>
diff --git a/main.js b/main.js
index 1882a1efe9c520402bdeb3d2d508b20af0ff32b7..e7ecf6937e4702c8d10635cbb68a8fbae142de15 100644 (file)
--- a/main.js
+++ b/main.js
@@ -115,6 +115,29 @@ function applyForceToFeather(game, vector) {
   game.objects.feather.speed.add(vector);
 }
 
+function start(game) {
+
+  function pinwheelPositionUpdate(game, viewportX, viewportY) {
+    const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
+    const viewportHeight = 2 * Math.tan(vFOV / 2) * (game.view.camera.position.z - game.objects.pinwheel.position.z);
+    game.objects.pinwheel.cameraX = viewportHeight * viewportX;
+    game.objects.pinwheel.cameraY = - viewportHeight * viewportY;
+  }
+
+  function cursorMoveEvent(canvasLocalX, canvasLocalY) {
+    const canvasBb = game.view.canvas.getBoundingClientRect();
+    // Intentional division by height instead of width in the following line, since
+    // three.js controls the vertical FOV. So if we ever change the aspect ratio from 1:1,
+    // y will still be in range (-0.5, 0.5), but the range for x will be smaller or larger.
+    const x = (canvasLocalX - canvasBb.x - (canvasBb.width / 2)) / canvasBb.height;
+    const y = (canvasLocalY - canvasBb.y - (canvasBb.height / 2)) / canvasBb.height;
+    pinwheelPositionUpdate(game, x, y);
+  }
+
+  document.body.addEventListener('mousemove', e => cursorMoveEvent(e.clientX, e.clientY));
+  document.body.addEventListener('touchmove', e => cursorMoveEvent(e.touches[0].clientX, e.touches[0].clientY));
+}
+
 function init(game, canvas) {
 
   game.timeProgress = 0;
@@ -123,6 +146,7 @@ function init(game, canvas) {
 
   game.objects = {};
   game.view = {};
+  game.view.canvas = canvas;
 
   const scene = new THREE.Scene();
   game.view.camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
@@ -327,153 +351,146 @@ function init(game, canvas) {
     game.objects.clouds.push(cloud);
   }
 
+  game.view.camera.position.set(-5, -game.courseRadius, game.view.camera.position.z);
+
   // All vectors used by the game loop (no allocations inside)
-  const featherLocalPos = new THREE.Vector3();
-  const featherBorderForce = new THREE.Vector3();
-  const pinwheelDistance = new THREE.Vector3();
-  const notCollectedPos = new THREE.Vector3();
-  const collectedPos = new THREE.Vector3();
-  const letterPos = new THREE.Vector3();
-  function animate() {
-    let delta = Math.min(game.view.clock.getDeltaTime(), 1 / 12);
+  game.var = {};
+  game.var.featherLocalPos = new THREE.Vector3();
+  game.var.featherBorderForce = new THREE.Vector3();
+  game.var.pinwheelDistance = new THREE.Vector3();
+  game.var.notCollectedPos = new THREE.Vector3();
+  game.var.collectedPos = new THREE.Vector3();
+  game.var.letterPos = new THREE.Vector3();
+  renderer.setAnimationLoop(() => { animate(game, renderer, scene); });
+}
 
-    game.timeProgress = (game.timeProgress + delta) % game.timeTotal; // play infinitely for now
-    const angle = 2 * Math.PI * (game.timeProgress / game.timeTotal);
-    game.view.camera.position.x = game.courseRadius * Math.sin(angle);
-    game.view.camera.position.y = - game.courseRadius * Math.cos(angle);
+function animate(game, renderer, scene) {
+  if(game.ui.currentPage != 'gameplay') {
+    game.view.camera.position.setY(-game.courseRadius + 0.1 * Math.sin(game.view.clock.getElapsedTime() * 0.5));
+    renderer.render(scene, game.view.camera);
+    return;
+  }
+  let delta = Math.min(game.view.clock.getDeltaTime(), 1 / 12);
 
-    const sunsetValue = 2.0 * easeInOut(Math.min(1, Math.max(0, ((game.timeProgress / game.timeTotal) - 0.3) / 0.6)));
-    for(let i = 0; i < 6; i++) {
-      game.view.materials['cloud' + i].uniforms.lerp.value = sunsetValue;
-    }
+  game.timeProgress = (game.timeProgress + delta) % game.timeTotal; // play infinitely for now
+  const angle = 2 * Math.PI * (game.timeProgress / game.timeTotal);
+  game.view.camera.position.x = game.courseRadius * Math.sin(angle);
+  game.view.camera.position.y = - game.courseRadius * Math.cos(angle);
 
-    featherLocalPos.subVectors(game.objects.feather.position, game.view.camera.position).setZ(0);
-    featherBorderForce.set(0, 0, 0);
-    for(let coord of [0, 1]) {
-      if(Math.abs(featherLocalPos.getComponent(coord)) > 3) {
-        featherBorderForce.setComponent(coord, 3 * Math.sign(featherLocalPos.getComponent(coord)) - featherLocalPos.getComponent(coord));
-      }
-    }
-    applyForceToFeather(game, featherBorderForce);
-    const tiltedGravity = game.gravity.clone();
-    pinwheelDistance.subVectors(game.objects.feather.position, game.objects.pinwheel.position).setZ(0);
+  const sunsetValue = 2.0 * easeInOut(Math.min(1, Math.max(0, ((game.timeProgress / game.timeTotal) - 0.3) / 0.6)));
+  for(let i = 0; i < 6; i++) {
+    game.view.materials['cloud' + i].uniforms.lerp.value = sunsetValue;
+  }
 
-    const pinwheelForce = 0.5 * Math.max(0, Math.pow(pinwheelDistance.length(), - 0.5) - 0.5);
-    applyForceToFeather(game, pinwheelDistance.normalize().multiplyScalar(pinwheelForce));
-    if(pinwheelForce < 0.2) {
-      if(game.objects.feather.swayDirection > 0 && game.objects.feather.speed.x > 1.5) {
-        game.objects.feather.swayDirection *= -1;
-      } else if(game.objects.feather.swayDirection < 0 && game.objects.feather.speed.x < -1.5) {
-        game.objects.feather.swayDirection *= -1;
-      }
-      tiltedGravity.x += game.objects.feather.swayDirection;
+  game.var.featherLocalPos.subVectors(game.objects.feather.position, game.view.camera.position).setZ(0);
+  game.var.featherBorderForce.set(0, 0, 0);
+  for(let coord of [0, 1]) {
+    if(Math.abs(game.var.featherLocalPos.getComponent(coord)) > 3) {
+      game.var.featherBorderForce.setComponent(coord, 3 * Math.sign(game.var.featherLocalPos.getComponent(coord)) - game.var.featherLocalPos.getComponent(coord));
     }
-    if(game.objects.feather.speed.y > -1) {
-      applyForceToFeather(game, tiltedGravity);
+  }
+  applyForceToFeather(game, game.var.featherBorderForce);
+  const tiltedGravity = game.gravity.clone();
+  game.var.pinwheelDistance.subVectors(game.objects.feather.position, game.objects.pinwheel.position).setZ(0);
+
+  const pinwheelForce = 0.5 * Math.max(0, Math.pow(game.var.pinwheelDistance.length(), - 0.5) - 0.5);
+  applyForceToFeather(game, game.var.pinwheelDistance.normalize().multiplyScalar(pinwheelForce));
+  if(pinwheelForce < 0.2) {
+    if(game.objects.feather.swayDirection > 0 && game.objects.feather.speed.x > 1.5) {
+      game.objects.feather.swayDirection *= -1;
+    } else if(game.objects.feather.swayDirection < 0 && game.objects.feather.speed.x < -1.5) {
+      game.objects.feather.swayDirection *= -1;
     }
-    game.objects.feather.speed.multiplyScalar(1 - delta);
-    game.objects.feather.rotation.z = -0.1 * game.objects.feather.speed.x;
-    game.objects.feather.position.addScaledVector(game.objects.feather.speed, delta);
+    tiltedGravity.x += game.objects.feather.swayDirection;
+  }
+  if(game.objects.feather.speed.y > -1) {
+    applyForceToFeather(game, tiltedGravity);
+  }
+  game.objects.feather.speed.multiplyScalar(1 - delta);
+  game.objects.feather.rotation.z = -0.1 * game.objects.feather.speed.x;
+  game.objects.feather.position.addScaledVector(game.objects.feather.speed, delta);
 
-    if(pinwheelForce > 0.2) {
-      if(game.objects.feather.twistSpeed < 0.0001) {
-        game.objects.feather.twistSpeed = (Math.random() - 0.5) * 0.01;
-      }
-      game.objects.feather.twistSpeed = Math.sign(game.objects.feather.twistSpeed) * 0.1 * game.objects.feather.speed.length();
-    } else {
-      game.objects.feather.twistSpeed = 0.98 * game.objects.feather.twistSpeed;
-      if(Math.abs(game.objects.feather.twistSpeed < 0.1)) {
-        let rotationDelta = game.objects.feather.rotation.x;
-        if(rotationDelta >= Math.PI) {
-          rotationDelta -= 2 * Math.PI;
-        }
-        game.objects.feather.twistSpeed -= rotationDelta * 0.02;
+  if(pinwheelForce > 0.2) {
+    if(game.objects.feather.twistSpeed < 0.0001) {
+      game.objects.feather.twistSpeed = (Math.random() - 0.5) * 0.01;
+    }
+    game.objects.feather.twistSpeed = Math.sign(game.objects.feather.twistSpeed) * 0.1 * game.objects.feather.speed.length();
+  } else {
+    game.objects.feather.twistSpeed = 0.98 * game.objects.feather.twistSpeed;
+    if(Math.abs(game.objects.feather.twistSpeed < 0.1)) {
+      let rotationDelta = game.objects.feather.rotation.x;
+      if(rotationDelta >= Math.PI) {
+        rotationDelta -= 2 * Math.PI;
       }
+      game.objects.feather.twistSpeed -= rotationDelta * 0.02;
     }
+  }
 
-    game.objects.feather.twistSpeed = Math.min(0.13, game.objects.feather.twistSpeed);
-    game.objects.feather.rotation.x = (game.objects.feather.rotation.x + game.objects.feather.twistSpeed) % (2 * Math.PI);
-    game.objects.pinwheel.rotation.z -= 5 * delta;
-    game.objects.pinwheel.position.x = game.view.camera.position.x + game.objects.pinwheel.cameraX;
-    game.objects.pinwheel.position.y = game.view.camera.position.y + game.objects.pinwheel.cameraY;
+  game.objects.feather.twistSpeed = Math.min(0.13, game.objects.feather.twistSpeed);
+  game.objects.feather.rotation.x = (game.objects.feather.rotation.x + game.objects.feather.twistSpeed) % (2 * Math.PI);
+  game.objects.pinwheel.rotation.z -= 5 * delta;
+  game.objects.pinwheel.position.x = game.view.camera.position.x + game.objects.pinwheel.cameraX;
+  game.objects.pinwheel.position.y = game.view.camera.position.y + game.objects.pinwheel.cameraY;
 
-    let collectedScale = lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
-    for(let i = 0; i < game.objects.words.length; i++) {
-      let word = game.objects.words[i];
-      if(!word.collected &&
-         game.objects.feather.speed.length() < 10.0 &&
-         new THREE.Vector3().subVectors(word.mapPos, game.objects.feather.position).length() < 1) {
-        word.collected = game.view.clock.getElapsedTime();
-        game.objects.words.collectedCount += 1;
+  let collectedScale = lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
+  for(let i = 0; i < game.objects.words.length; i++) {
+    let word = game.objects.words[i];
+    if(!word.collected &&
+       game.objects.feather.speed.length() < 10.0 &&
+       new THREE.Vector3().subVectors(word.mapPos, game.objects.feather.position).length() < 1) {
+      word.collected = game.view.clock.getElapsedTime();
+      game.objects.words.collectedCount += 1;
+    }
+    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.mapPos.x + wordAnimationRadius * Math.cos(animationProgress * 5 * Math.PI * 2);
+        y = word.mapPos.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);
       }
-      let x, y, z;
-      let collectionAnimationDuration = 1.0;
-      for(let j = 0; j < word.children.length; j++) {
-        notCollectedPos.set(0, 0, 0);
-        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.mapPos.x + wordAnimationRadius * Math.cos(animationProgress * 5 * Math.PI * 2);
-          y = word.mapPos.y + wordAnimationRadius * Math.sin(animationProgress * 4 * Math.PI * 2);
-          z = wordAnimationRadius * Math.sin(animationProgress * 6 * Math.PI * 2);
-          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;
-          collectedPos.set(x, y, z);
-        }
-        if(notCollectedPos.length() > 0 && collectedPos.length() > 0) {
-          let collectingProgress = easeInOut(Math.max(0.0, Math.min(1.0, (game.view.clock.getElapsedTime() - word.collected) / collectionAnimationDuration)));
-          letterPos.lerpVectors(notCollectedPos, collectedPos, collectingProgress);
-          let scale = lerp(1.0, collectedScale, collectingProgress);
-          letter.scale.set(scale, scale, scale);
-        } else if(notCollectedPos.length() > 0) {
-          letterPos.set(notCollectedPos.x, notCollectedPos.y, notCollectedPos.z);
-        } else if(collectedPos.length() > 0) {
-          letterPos.set(collectedPos.x, collectedPos.y, collectedPos.z);
-        }
-        letter.position.set(letterPos.x, letterPos.y, letterPos.z);
-        let rotation = (game.timeProgress * 3) % (2 * Math.PI);
-        letter.rotation.set(rotation + j, rotation + 2*j, rotation + 3*j);
+      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;
+        game.var.collectedPos.set(x, y, z);
+      }
+      if(game.var.notCollectedPos.length() > 0 && game.var.collectedPos.length() > 0) {
+        let collectingProgress = easeInOut(Math.max(0.0, Math.min(1.0, (game.view.clock.getElapsedTime() - word.collected) / collectionAnimationDuration)));
+        game.var.letterPos.lerpVectors(game.var.notCollectedPos, game.var.collectedPos, collectingProgress);
+        let scale = lerp(1.0, collectedScale, collectingProgress);
+        letter.scale.set(scale, scale, scale);
+      } else if(game.var.notCollectedPos.length() > 0) {
+        game.var.letterPos.set(game.var.notCollectedPos.x, game.var.notCollectedPos.y, game.var.notCollectedPos.z);
+      } else if(game.var.collectedPos.length() > 0) {
+        game.var.letterPos.set(game.var.collectedPos.x, game.var.collectedPos.y, game.var.collectedPos.z);
       }
+      letter.position.set(game.var.letterPos.x, game.var.letterPos.y, game.var.letterPos.z);
+      let rotation = (game.timeProgress * 3) % (2 * Math.PI);
+      letter.rotation.set(rotation + j, rotation + 2*j, rotation + 3*j);
     }
-
-    renderer.render(scene, game.view.camera);
-  }
-  renderer.setAnimationLoop(animate);
-
-  function pinwheelPositionUpdate(game, viewportX, viewportY) {
-    const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
-    const viewportHeight = 2 * Math.tan(vFOV / 2) * (game.view.camera.position.z - game.objects.pinwheel.position.z);
-    game.objects.pinwheel.cameraX = viewportHeight * viewportX;
-    game.objects.pinwheel.cameraY = - viewportHeight * viewportY;
   }
 
-  function cursorMoveEvent(canvasLocalX, canvasLocalY) {
-    const canvasBb = canvas.getBoundingClientRect();
-    // Intentional division by height instead of width in the following line, since
-    // three.js controls the vertical FOV. So if we ever change the aspect ratio from 1:1,
-    // y will still be in range (-0.5, 0.5), but the range for x will be smaller or larger.
-    const x = (canvasLocalX - canvasBb.x - (canvasBb.width / 2)) / canvasBb.height;
-    const y = (canvasLocalY - canvasBb.y - (canvasBb.height / 2)) / canvasBb.height;
-    pinwheelPositionUpdate(game, x, y);
-  }
-
-  document.body.addEventListener('mousemove', e => cursorMoveEvent(e.clientX, e.clientY));
-  document.body.addEventListener('touchmove', e => cursorMoveEvent(e.touches[0].clientX, e.touches[0].clientY));
+  renderer.render(scene, game.view.camera);
 }
 
 document.querySelector('#enableMusic').checked = false;
+document.querySelector('.startGame').addEventListener('click', () => {
+  game.ui.moveToPage('gameplay');
+  start(game);
+});
 window['game'] = {
   state: 'loadingAssets',
   ui: {
@@ -489,19 +506,27 @@ game.ui.moveToPage = (target) => {
       page.style.display = 'none';
     }, fadeDuration, page);
   });
-  const targetElem = game.ui.root.querySelector('.ui-page.' + target + '');
-  targetElem.style.opacity = '0';
-  targetElem.style.display = 'flex';
-  setTimeout((targetElem) => {
-    targetElem.style.opacity = '1';
-  }, fadeDuration, targetElem);
+  const targetElems = [game.ui.root.querySelector('.ui-page.' + target + '')];
+  if(game.ui.root.querySelector('.ui-page.gameplay').style.opacity != '1') {
+    targetElems.push(game.ui.root.querySelector('.ui-page.gameplay'));
+  }
+  for(let targetElem of targetElems) {
+    if(!targetElem.classList.contains('gameplay')) {
+      targetElem.style.opacity = '0';
+    }
+    targetElem.style.display = 'flex';
+    setTimeout((targetElem) => {
+      targetElem.style.opacity = '1';
+    }, fadeDuration, targetElem);
+  }
+  game.ui.currentPage = target;
 };
 loadAllAssets(game, (progress) => {
   let percentage = Math.floor(100 * progress);
   document.querySelector('.ui-page.loading progress').value = percentage;
   document.querySelector('.ui-page.loading span').innerText = percentage;
 }).then(() => {
-  game.ui.moveToPage('gameplay');
+  game.ui.moveToPage('title');
   init(window['game'], document.querySelector('canvas'));
   document.querySelector('#enableMusic').addEventListener('change', () => toggleMusic(window['game']));
 }, (err) => {