Spaces:
Running on Zero
Running on Zero
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- frontend/index.html +1 -1
- frontend/main.js +52 -16
- frontend/style.css +43 -8
- 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
|
| 30 |
-
|
|
|
|
|
|
|
| 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(
|
| 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 =
|
| 103 |
-
const DESCENT_MS =
|
| 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 |
-
|
| 113 |
}
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
const easeInOutCubic = (t) =>
|
| 118 |
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
| 119 |
|
| 120 |
-
// Dev
|
| 121 |
window.lofinityDebug = {
|
| 122 |
skipIntro() {
|
| 123 |
intro.phase = "idle";
|
|
|
|
|
|
|
| 124 |
lookTarget.copy(LOOK_SCENE);
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 === "
|
|
|
|
|
|
|
|
|
|
| 145 |
const t = Math.min((performance.now() - intro.t0) / DESCENT_MS, 1);
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
} else if (intro.phase === "idle") {
|
| 149 |
-
// Gentle lofi sway
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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(
|
| 38 |
color: #ffffff;
|
| 39 |
margin: 0;
|
| 40 |
-
letter-spacing: 0.
|
| 41 |
-
text-shadow:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
#subtitle {
|
| 45 |
font-family: "Baloo 2", sans-serif;
|
| 46 |
font-weight: 600;
|
| 47 |
-
font-size: clamp(
|
| 48 |
-
color: rgba(255, 255, 255, 0.
|
| 49 |
-
margin:
|
| 50 |
-
letter-spacing: 0.
|
| 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.
|
| 725 |
});
|
| 726 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
function makeCloud(scale) {
|
|
|
|
|
|
|
| 728 |
const cloud = new THREE.Group();
|
| 729 |
-
const
|
|
|
|
|
|
|
| 730 |
for (let i = 0; i < puffs; i++) {
|
| 731 |
-
const
|
| 732 |
-
const
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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(
|
| 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 |
|