eloigil6 commited on
Commit
9290e75
·
1 Parent(s): 76a98b2

Enhance visual presentation and camera dynamics. Updated subtitle styling in index.html, refined camera movement and lighting in main.js, and improved cloud generation in world.js. Adjusted CSS for smoother transitions and animations, enhancing overall user experience.

Browse files
Files changed (4) hide show
  1. frontend/index.html +1 -1
  2. frontend/main.js +52 -16
  3. frontend/style.css +43 -8
  4. frontend/world.js +43 -12
frontend/index.html CHANGED
@@ -19,7 +19,7 @@
19
  <body>
20
  <div id="title-overlay">
21
  <h1 id="title">LoFinity</h1>
22
- <p id="subtitle">chill beats, freshly vended</p>
23
  </div>
24
  <canvas id="scene"></canvas>
25
  <script type="module" src="/static/main.js"></script>
 
19
  <body>
20
  <div id="title-overlay">
21
  <h1 id="title">LoFinity</h1>
22
+ <p id="subtitle">chill beats, freshly vended</p>
23
  </div>
24
  <canvas id="scene"></canvas>
25
  <script type="module" src="/static/main.js"></script>
frontend/main.js CHANGED
@@ -26,12 +26,15 @@ const camera = new THREE.PerspectiveCamera(
26
  1000
27
  );
28
 
29
- // The camera stays put; we animate what it looks at.
30
- const CAMERA_POS = new THREE.Vector3(0, 4.8, 17);
 
 
31
  const LOOK_SKY = new THREE.Vector3(0, 90, -60);
32
  const LOOK_SCENE = new THREE.Vector3(-0.5, 2.8, -3);
33
- camera.position.copy(CAMERA_POS);
34
  camera.lookAt(LOOK_SKY);
 
35
 
36
  window.addEventListener("resize", () => {
37
  camera.aspect = window.innerWidth / window.innerHeight;
@@ -76,7 +79,14 @@ scene.add(new THREE.Mesh(new THREE.SphereGeometry(450, 32, 16), skyMaterial));
76
 
77
  scene.add(new THREE.HemisphereLight(0xcfe8ff, 0xa8b48a, 0.85));
78
 
 
 
 
 
 
 
79
  const sun = new THREE.DirectionalLight(0xfff2dc, 1.6);
 
80
  sun.position.set(30, 55, 20);
81
  sun.castShadow = true;
82
  sun.shadow.mapSize.set(2048, 2048);
@@ -99,30 +109,46 @@ const world = buildWorld(scene);
99
  // Intro: hold on the sky, then ease the gaze down to the scene
100
  // ---------------------------------------------------------------------------
101
 
102
- const HOLD_MS = 2800; // sky time after everything has loaded
103
- const DESCENT_MS = 3400;
104
 
105
- const intro = { phase: "loading", t0: 0 };
106
  const lookTarget = LOOK_SKY.clone();
 
107
 
108
  function startDescent() {
109
  if (intro.phase !== "loading") return;
110
  intro.phase = "descending";
111
  intro.t0 = performance.now();
112
- document.getElementById("title-overlay").classList.add("hidden");
113
  }
114
 
115
- window.addEventListener("load", () => setTimeout(startDescent, HOLD_MS));
 
 
 
 
 
 
 
116
 
117
  const easeInOutCubic = (t) =>
118
  t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
119
 
120
- // Dev helper: jump straight to the post-intro state.
121
  window.lofinityDebug = {
122
  skipIntro() {
123
  intro.phase = "idle";
 
 
124
  lookTarget.copy(LOOK_SCENE);
125
- document.getElementById("title-overlay").classList.add("hidden");
 
 
 
 
 
 
126
  },
127
  };
128
 
@@ -141,14 +167,24 @@ function animate() {
141
  if (cloud.position.x > 170) cloud.position.x = -170;
142
  }
143
 
144
- if (intro.phase === "descending") {
 
 
 
145
  const t = Math.min((performance.now() - intro.t0) / DESCENT_MS, 1);
146
- lookTarget.lerpVectors(LOOK_SKY, LOOK_SCENE, easeInOutCubic(t));
147
- if (t >= 1) intro.phase = "idle";
 
 
 
 
 
148
  } else if (intro.phase === "idle") {
149
- // Gentle lofi sway
150
- camera.position.x = CAMERA_POS.x + Math.sin(elapsed * 0.25) * 0.25;
151
- camera.position.y = CAMERA_POS.y + Math.sin(elapsed * 0.4) * 0.12;
 
 
152
  }
153
 
154
  camera.lookAt(lookTarget);
 
26
  1000
27
  );
28
 
29
+ // The camera starts high in the sky gaze and dollies down/back during the
30
+ // descent for a touch of parallax.
31
+ const CAM_START = new THREE.Vector3(0, 7.4, 14.5);
32
+ const CAM_END = new THREE.Vector3(0, 4.8, 17);
33
  const LOOK_SKY = new THREE.Vector3(0, 90, -60);
34
  const LOOK_SCENE = new THREE.Vector3(-0.5, 2.8, -3);
35
+ camera.position.copy(CAM_START);
36
  camera.lookAt(LOOK_SKY);
37
+ camera.layers.enable(1); // cloud layer
38
 
39
  window.addEventListener("resize", () => {
40
  camera.aspect = window.innerWidth / window.innerHeight;
 
79
 
80
  scene.add(new THREE.HemisphereLight(0xcfe8ff, 0xa8b48a, 0.85));
81
 
82
+ // Clouds (layer 1) get their own bounce light: white from above, cool
83
+ // blue-gray from below — no green ground tint.
84
+ const cloudLight = new THREE.HemisphereLight(0xf2f8ff, 0xc9d9ec, 0.9);
85
+ cloudLight.layers.set(1);
86
+ scene.add(cloudLight);
87
+
88
  const sun = new THREE.DirectionalLight(0xfff2dc, 1.6);
89
+ sun.layers.enable(1);
90
  sun.position.set(30, 55, 20);
91
  sun.castShadow = true;
92
  sun.shadow.mapSize.set(2048, 2048);
 
109
  // Intro: hold on the sky, then ease the gaze down to the scene
110
  // ---------------------------------------------------------------------------
111
 
112
+ const HOLD_MS = 3000; // sky time after everything (fonts included) has loaded
113
+ const DESCENT_MS = 4600;
114
 
115
+ const intro = { phase: "loading", t0: 0, idleStart: 0 };
116
  const lookTarget = LOOK_SKY.clone();
117
+ const overlay = document.getElementById("title-overlay");
118
 
119
  function startDescent() {
120
  if (intro.phase !== "loading") return;
121
  intro.phase = "descending";
122
  intro.t0 = performance.now();
123
+ overlay.classList.add("hidden");
124
  }
125
 
126
+ const pageLoaded = new Promise((resolve) =>
127
+ document.readyState === "complete"
128
+ ? resolve()
129
+ : window.addEventListener("load", resolve)
130
+ );
131
+ Promise.all([pageLoaded, document.fonts.ready]).then(() =>
132
+ setTimeout(startDescent, HOLD_MS)
133
+ );
134
 
135
  const easeInOutCubic = (t) =>
136
  t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
137
 
138
+ // Dev helpers: jump straight to either end of the intro.
139
  window.lofinityDebug = {
140
  skipIntro() {
141
  intro.phase = "idle";
142
+ intro.idleStart = 0;
143
+ camera.position.copy(CAM_END);
144
  lookTarget.copy(LOOK_SCENE);
145
+ overlay.classList.add("hidden");
146
+ },
147
+ holdSky() {
148
+ intro.phase = "held";
149
+ camera.position.copy(CAM_START);
150
+ lookTarget.copy(LOOK_SKY);
151
+ overlay.classList.remove("hidden");
152
  },
153
  };
154
 
 
167
  if (cloud.position.x > 170) cloud.position.x = -170;
168
  }
169
 
170
+ if (intro.phase === "loading" || intro.phase === "held") {
171
+ // Barely-there float while gazing at the sky
172
+ camera.position.y = CAM_START.y + Math.sin(elapsed * 0.5) * 0.08;
173
+ } else if (intro.phase === "descending") {
174
  const t = Math.min((performance.now() - intro.t0) / DESCENT_MS, 1);
175
+ const e = easeInOutCubic(t);
176
+ camera.position.lerpVectors(CAM_START, CAM_END, e);
177
+ lookTarget.lerpVectors(LOOK_SKY, LOOK_SCENE, e);
178
+ if (t >= 1) {
179
+ intro.phase = "idle";
180
+ intro.idleStart = elapsed;
181
+ }
182
  } else if (intro.phase === "idle") {
183
+ // Gentle lofi sway, ramping in so the descent lands without a jolt
184
+ const tSway = elapsed - intro.idleStart;
185
+ const ramp = Math.min(1, tSway / 5);
186
+ camera.position.x = CAM_END.x + Math.sin(tSway * 0.25) * 0.25 * ramp;
187
+ camera.position.y = CAM_END.y + Math.sin(tSway * 0.4) * 0.12 * ramp;
188
  }
189
 
190
  camera.lookAt(lookTarget);
frontend/style.css CHANGED
@@ -24,29 +24,64 @@ body {
24
  pointer-events: none;
25
  z-index: 10;
26
  opacity: 1;
27
- transition: opacity 1.4s ease;
 
28
  }
29
 
30
  #title-overlay.hidden {
31
  opacity: 0;
 
32
  }
33
 
34
  #title {
35
  font-family: "Baloo 2", sans-serif;
36
  font-weight: 800;
37
- font-size: clamp(3rem, 10vw, 7rem);
38
  color: #ffffff;
39
  margin: 0;
40
- letter-spacing: 0.04em;
41
- text-shadow: 0 4px 24px rgba(20, 60, 140, 0.45);
 
 
 
 
 
 
42
  }
43
 
44
  #subtitle {
45
  font-family: "Baloo 2", sans-serif;
46
  font-weight: 600;
47
- font-size: clamp(1rem, 2.4vw, 1.4rem);
48
- color: rgba(255, 255, 255, 0.85);
49
- margin: 0.4rem 0 0;
50
- letter-spacing: 0.12em;
51
  text-transform: lowercase;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
 
24
  pointer-events: none;
25
  z-index: 10;
26
  opacity: 1;
27
+ transform: translateY(0);
28
+ transition: opacity 1.8s ease, transform 1.8s cubic-bezier(0.55, 0, 0.55, 1);
29
  }
30
 
31
  #title-overlay.hidden {
32
  opacity: 0;
33
+ transform: translateY(-48px);
34
  }
35
 
36
  #title {
37
  font-family: "Baloo 2", sans-serif;
38
  font-weight: 800;
39
+ font-size: clamp(3.5rem, 11vw, 8rem);
40
  color: #ffffff;
41
  margin: 0;
42
+ letter-spacing: 0.06em;
43
+ text-shadow:
44
+ 0 3px 0 rgba(173, 211, 255, 0.55),
45
+ 0 8px 28px rgba(13, 47, 110, 0.5),
46
+ 0 2px 6px rgba(13, 47, 110, 0.35);
47
+ animation:
48
+ title-in 1.6s cubic-bezier(0.22, 1, 0.36, 1) both,
49
+ title-bob 5.5s ease-in-out 1.6s infinite alternate;
50
  }
51
 
52
  #subtitle {
53
  font-family: "Baloo 2", sans-serif;
54
  font-weight: 600;
55
+ font-size: clamp(0.95rem, 2.2vw, 1.3rem);
56
+ color: rgba(255, 255, 255, 0.95);
57
+ margin: 1.1rem 0 0;
58
+ letter-spacing: 0.14em;
59
  text-transform: lowercase;
60
+ padding: 0.45em 1.4em;
61
+ border-radius: 999px;
62
+ background: rgba(255, 255, 255, 0.14);
63
+ border: 1px solid rgba(255, 255, 255, 0.3);
64
+ backdrop-filter: blur(6px);
65
+ -webkit-backdrop-filter: blur(6px);
66
+ animation: title-in 1.6s cubic-bezier(0.22, 1, 0.36, 1) 0.5s both;
67
+ }
68
+
69
+ @keyframes title-in {
70
+ from {
71
+ opacity: 0;
72
+ transform: translateY(28px) scale(0.96);
73
+ }
74
+ to {
75
+ opacity: 1;
76
+ transform: translateY(0) scale(1);
77
+ }
78
+ }
79
+
80
+ @keyframes title-bob {
81
+ from {
82
+ transform: translateY(0);
83
+ }
84
+ to {
85
+ transform: translateY(-12px);
86
+ }
87
  }
frontend/world.js CHANGED
@@ -717,45 +717,76 @@ function buildMountains() {
717
  }
718
 
719
  function buildClouds() {
 
 
 
720
  const cloudMaterial = new THREE.MeshLambertMaterial({
721
  color: 0xffffff,
722
  flatShading: true,
723
  emissive: 0xdde9f4,
724
- emissiveIntensity: 0.38,
725
  });
726
 
 
 
 
 
 
 
 
 
 
727
  function makeCloud(scale) {
 
 
728
  const cloud = new THREE.Group();
729
- const puffs = 5 + Math.floor(Math.random() * 4);
 
 
730
  for (let i = 0; i < puffs; i++) {
731
- const r = rand(3.5, 8);
732
- const puff = new THREE.Mesh(new THREE.IcosahedronGeometry(r, 1), cloudMaterial);
733
- puff.position.set(
734
- (i - puffs / 2) * (r * 0.85) + rand(-1.5, 1.5),
735
- rand(-1.5, 1.5),
736
- rand(-2, 2)
 
 
 
 
 
 
 
 
737
  );
738
- puff.scale.y = 0.55;
739
- cloud.add(puff);
740
  }
 
 
741
  cloud.scale.setScalar(scale);
742
  return cloud;
743
  }
744
 
745
  const clouds = [];
746
  // big hero cumulus, center-back like the reference
747
- for (const [x, y, z, s] of [[15, 55, -150, 2.6], [-5, 48, -140, 1.8]]) {
748
  const cloud = makeCloud(s);
749
  cloud.position.set(x, y, z);
750
  cloud.userData.speed = 0.25;
751
  clouds.push(cloud);
752
  }
753
  for (let i = 0; i < 11; i++) {
754
- const cloud = makeCloud(rand(0.7, 1.5));
755
  cloud.position.set(rand(-150, 150), rand(34, 80), rand(-170, -50));
756
  cloud.userData.speed = rand(0.4, 1.2);
757
  clouds.push(cloud);
758
  }
 
 
 
 
 
 
 
759
  return clouds;
760
  }
761
 
 
717
  }
718
 
719
  function buildClouds() {
720
+ // Clouds live on light layer 1: they are lit by the sun plus a dedicated
721
+ // cool hemisphere light (set up in main.js), so they never pick up the
722
+ // green ground bounce that tints layer-0 objects.
723
  const cloudMaterial = new THREE.MeshLambertMaterial({
724
  color: 0xffffff,
725
  flatShading: true,
726
  emissive: 0xdde9f4,
727
+ emissiveIntensity: 0.25,
728
  });
729
 
730
+ function addPuff(cloud, r, x, y, z) {
731
+ const puff = new THREE.Mesh(new THREE.IcosahedronGeometry(r, 1), cloudMaterial);
732
+ puff.position.set(x, y, z);
733
+ puff.scale.y = 0.66;
734
+ puff.layers.set(1);
735
+ cloud.add(puff);
736
+ return puff;
737
+ }
738
+
739
  function makeCloud(scale) {
740
+ // Chunky anime cumulus: big heavily-overlapping puffs (fat middle,
741
+ // flat-ish underside) with a crown of medium puffs nestled on top.
742
  const cloud = new THREE.Group();
743
+ const length = rand(10, 16);
744
+ const puffs = Math.max(4, Math.round(length / 2.6));
745
+ let maxR = 0;
746
  for (let i = 0; i < puffs; i++) {
747
+ const t = puffs === 1 ? 0.5 : i / (puffs - 1);
748
+ const envelope = 0.55 + 0.45 * Math.sin(Math.PI * t);
749
+ const r = (3.2 + 3.2 * envelope) * rand(0.9, 1.1);
750
+ maxR = Math.max(maxR, r);
751
+ addPuff(cloud, r, (t - 0.5) * length + rand(-0.5, 0.5), r * 0.5, rand(-1.8, 1.8));
752
+ }
753
+ const crowns = 2 + Math.floor(Math.random() * 2);
754
+ for (let j = 0; j < crowns; j++) {
755
+ addPuff(
756
+ cloud,
757
+ rand(2.4, 3.6),
758
+ rand(-length * 0.22, length * 0.22),
759
+ maxR * rand(0.8, 0.95),
760
+ rand(-1.2, 1.2)
761
  );
 
 
762
  }
763
+ // stay mostly side-on so clouds keep their silhouette from the camera
764
+ cloud.rotation.y = rand(-0.35, 0.35);
765
  cloud.scale.setScalar(scale);
766
  return cloud;
767
  }
768
 
769
  const clouds = [];
770
  // big hero cumulus, center-back like the reference
771
+ for (const [x, y, z, s] of [[15, 55, -150, 2.6], [-5, 48, -140, 2.0]]) {
772
  const cloud = makeCloud(s);
773
  cloud.position.set(x, y, z);
774
  cloud.userData.speed = 0.25;
775
  clouds.push(cloud);
776
  }
777
  for (let i = 0; i < 11; i++) {
778
+ const cloud = makeCloud(rand(1.0, 1.6));
779
  cloud.position.set(rand(-150, 150), rand(34, 80), rand(-170, -50));
780
  cloud.userData.speed = rand(0.4, 1.2);
781
  clouds.push(cloud);
782
  }
783
+ // high clouds that frame the title during the sky-gaze intro
784
+ for (let i = 0; i < 6; i++) {
785
+ const cloud = makeCloud(rand(1.3, 2.1));
786
+ cloud.position.set(rand(-130, 130), rand(85, 130), rand(-200, -90));
787
+ cloud.userData.speed = rand(0.3, 0.8);
788
+ clouds.push(cloud);
789
+ }
790
  return clouds;
791
  }
792