Improve MineGeo UI and application flows
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s

This commit is contained in:
2026-10-18 11:44:04 +05:00
parent ad73132f15
commit a7085e4e80
50 changed files with 4790 additions and 703 deletions

View File

@@ -1,4 +1,6 @@
import * as THREE from "three";
import { composeFrame } from "./composition.mjs";
import { easeInOut, enter } from "./motion.mjs";
const params = new URLSearchParams(location.search);
const banner = params.get("mode") === "banner";
@@ -259,364 +261,61 @@ const screenshots = banner
}),
),
);
const clamp = (n, a = 0, b = 1) => Math.max(a, Math.min(b, n));
const smooth = (n) => {
const v = clamp(n);
return v * v * (3 - 2 * v);
};
function text(value, x, y, size, color = "#f0f2da", align = "left") {
ctx.font = `${size}px Pixel`;
ctx.textAlign = align;
ctx.fillStyle = color;
ctx.fillText(value, x, y);
}
function label(value, x, y, size = 12, color = "#9cb59e") {
ctx.font = `${size}px Arial`;
ctx.textAlign = "left";
ctx.fillStyle = color;
ctx.letterSpacing = "3px";
ctx.fillText(value, x, y);
ctx.letterSpacing = "0px";
}
function cross(x, y, size = 5) {
ctx.beginPath();
ctx.moveTo(x - size, y);
ctx.lineTo(x + size, y);
ctx.moveTo(x, y - size);
ctx.lineTo(x, y + size);
ctx.stroke();
}
function compass(x, y, radius, angle, opacity = 0.7) {
ctx.save();
ctx.translate(x, y);
ctx.globalAlpha *= opacity;
ctx.strokeStyle = "#b5c8a3";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.stroke();
for (let i = 0; i < 48; i++) {
const a = (i / 48) * Math.PI * 2;
const inner = radius - (i % 12 === 0 ? 12 : i % 3 === 0 ? 7 : 3);
ctx.beginPath();
ctx.moveTo(Math.cos(a) * inner, Math.sin(a) * inner);
ctx.lineTo(Math.cos(a) * radius, Math.sin(a) * radius);
ctx.stroke();
}
ctx.rotate(angle);
ctx.fillStyle = "#dfb76e";
ctx.beginPath();
ctx.moveTo(0, -radius * 0.58);
ctx.lineTo(radius * 0.14, 0);
ctx.lineTo(0, radius * 0.12);
ctx.lineTo(-radius * 0.14, 0);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#526e60";
ctx.beginPath();
ctx.moveTo(0, radius * 0.58);
ctx.lineTo(radius * 0.14, 0);
ctx.lineTo(0, -radius * 0.12);
ctx.lineTo(-radius * 0.14, 0);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function overlay(t) {
ctx.clearRect(0, 0, width, height);
ctx.drawImage(renderer.domElement, 0, 0);
const scale = width / (banner ? 1200 : 1280);
ctx.save();
ctx.scale(scale, scale);
const w = width / scale,
h = height / scale;
const gradient = ctx.createLinearGradient(0, 0, w, 0);
gradient.addColorStop(0, "rgba(5,16,12,.95)");
gradient.addColorStop(0.38, "rgba(5,16,12,.78)");
gradient.addColorStop(0.75, "rgba(5,16,12,0)");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
const vignette = ctx.createRadialGradient(
w * 0.6,
h * 0.42,
h * 0.2,
w * 0.5,
h * 0.5,
w * 0.7,
);
vignette.addColorStop(0, "rgba(3,12,9,0)");
vignette.addColorStop(1, "rgba(3,12,9,.7)");
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = "rgba(161,193,151,.25)";
ctx.lineWidth = 1;
cross(32, 32);
cross(w - 32, 32);
cross(32, h - 32);
cross(w - 32, h - 32);
if (banner) {
label("A MINECRAFT GUESSING GAME", 62, 118, 13);
text("MINE", 58, 231, 78);
text("GEO", 297, 231, 78, "#b3dd85");
text("GET LOST. FIND YOUR WORLD.", 62, 280, 16, "#c4d1b5");
ctx.fillStyle = "#b6d790";
ctx.fillRect(62, 326, 28, 2);
label("MINEGEO.SITE", 105, 333, 13, "#b6d790");
compass(w - 70, 72, 28, -0.5 + Math.sin((t * Math.PI * 2) / 3) * 0.1);
label("N", w - 75, 30, 10, "#dfb76e");
} else {
const scenes = [
{
start: 0,
end: 4,
kicker: "SOMEWHERE IN THE OVERWORLD",
lines: ["EVERY WORLD", "HAS A PLACE."],
sub: "But can you find yours?",
size: 62,
},
{
start: 4,
end: 9,
kicker: "01 / EXPLORE",
lines: ["YOUR EXPEDITION STARTS HERE."],
sub: "The real MineGeo website. A new way to see Minecraft.",
image: "home",
size: 62,
},
{
start: 9,
end: 14,
kicker: "02 / DISCOVER",
lines: ["PICK YOUR WORLD."],
sub: "Browse published maps from the community.",
image: "catalog",
size: 68,
},
{
start: 14,
end: 19,
kicker: "03 / MAKE YOUR GUESS",
lines: ["LOOK. GUESS. DISCOVER."],
sub: "Real in-game capture. Look around, then place your pin.",
image: "game",
size: 62,
},
{
start: 19,
end: 24,
kicker: "04 / GO FURTHER",
lines: ["MAKE IT YOUR EXPEDITION."],
sub: "Choose your map, difficulty, and game mode.",
image: "map",
size: 62,
},
{
start: 24,
end: 30,
kicker: "YOUR NEXT ADVENTURE STARTS HERE",
lines: ["MINEGEO"],
sub: "Get lost in a familiar world.",
size: 82,
},
];
const shot = scenes.find((s) => t >= s.start && t < s.end) || scenes.at(-1);
const local = t - shot.start;
const alpha =
smooth(local / 0.55) *
(shot.end === 30 ? 1 : 1 - smooth((t - (shot.end - 0.5)) / 0.5));
ctx.save();
ctx.globalAlpha = alpha;
const rise = (1 - smooth(local / 0.8)) * 16;
if (shot.image) {
label(shot.kicker, 65, 58, 11, "#b0cb92");
text(shot.lines[0], 62, 104 + rise, 30, "#e4edcf");
ctx.font = "14px Arial";
ctx.fillStyle = "#b9cbb4";
ctx.fillText(shot.sub, 65, 128 + rise);
const image = screenshots[shot.image];
const panelWidth = 950,
cropHeight = image.naturalHeight * 0.93;
const imageHeight = (cropHeight / image.naturalWidth) * panelWidth;
const x = (w - panelWidth) / 2,
y = 164 + (1 - smooth(local / 0.8)) * 18;
const grow = 0.98 + smooth(local / 5) * 0.02;
ctx.translate(w / 2, y + imageHeight / 2);
ctx.scale(grow, grow);
ctx.translate(-w / 2, -y - imageHeight / 2);
ctx.shadowColor = "rgba(0,0,0,.8)";
ctx.shadowBlur = 36;
ctx.shadowOffsetY = 12;
ctx.fillStyle = "#14241e";
ctx.beginPath();
ctx.roundRect(x - 1, y - 25, panelWidth + 2, imageHeight + 26, 9);
ctx.fill();
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
ctx.strokeStyle = "#658366";
ctx.stroke();
for (let i = 0; i < 3; i++) {
ctx.fillStyle = ["#ad7966", "#bda169", "#83a374"][i];
ctx.beginPath();
ctx.arc(x + 15 + i * 12, y - 12, 3, 0, Math.PI * 2);
ctx.fill();
}
label(
`MINEGEO.SITE / ${shot.image === "game" ? "PLAY" : shot.image === "home" ? "HOME" : "WORLDS"}`,
x + 74,
y - 8,
8,
"#b6c5af",
);
ctx.save();
ctx.beginPath();
ctx.rect(x, y, panelWidth, imageHeight);
ctx.clip();
const zoom = 1 + local * 0.004;
ctx.drawImage(
image,
0,
0,
image.naturalWidth,
cropHeight,
x - (panelWidth * (zoom - 1)) / 2,
y - (imageHeight * (zoom - 1)) / 2,
panelWidth * zoom,
imageHeight * zoom,
);
ctx.restore();
} else {
label(shot.kicker, 64, 225 + rise, 12, "#b0cb92");
shot.lines.forEach((line, i) =>
text(
line,
60,
320 + i * 76 + rise,
shot.size,
i === shot.lines.length - 1 ? "#b3dd85" : "#f0f2da",
),
);
const subY = shot.lines.length === 1 ? 390 : 444;
ctx.font = "18px Arial";
ctx.textAlign = "left";
ctx.fillStyle = "#becdb9";
ctx.fillText(shot.sub, 65, subY + rise);
if (shot.start === 24) {
ctx.strokeStyle = "#87a768";
ctx.strokeRect(65, 432, 260, 54);
text("MINEGEO.SITE", 88, 467, 20, "#d7e7c1");
label("FREE TO EXPLORE", 65, 528, 10);
}
}
ctx.restore();
if (!shot.image) label("MINEGEO / FIELD NOTES", 65, 65, 11);
label(
`${String(Math.floor(t)).padStart(2, "0")} : ${String(Math.floor((t % 1) * 24)).padStart(2, "0")} / 30`,
w - 245,
shot.image ? h - 9 : h - 49,
10,
);
text("N", w - 92, 62, 12, "#dbc18c");
compass(w - 88, 115, 34, -t * 0.08);
ctx.strokeStyle = "rgba(168,195,147,.22)";
ctx.beginPath();
ctx.moveTo(65, h - (shot.image ? 27 : 72));
ctx.lineTo(w - 65, h - (shot.image ? 27 : 72));
ctx.stroke();
ctx.strokeStyle = "#afce83";
ctx.beginPath();
ctx.moveTo(65, h - (shot.image ? 27 : 72));
ctx.lineTo(65 + ((w - 130) * t) / 30, h - (shot.image ? 27 : 72));
ctx.stroke();
if (!shot.image && t > 14 && t < 19) {
const a = smooth((t - 14) / 0.5) * (1 - smooth((t - 18.5) / 0.5));
ctx.globalAlpha = a;
label("PIN PLACED", w - 250, h - 120, 12, "#f0d19b");
ctx.globalAlpha = 1;
}
// Very short dark dissolves separate the six camera moves.
const cut = [4, 9, 14, 19, 24].reduce(
(a, c) => Math.max(a, 1 - Math.abs(t - c) / 0.2),
0,
);
const fadeIn = 1 - smooth(t / 0.7),
fadeOut = smooth((t - 29.3) / 0.7);
ctx.fillStyle = `rgba(5,14,10,${Math.max(cut, fadeIn, fadeOut)})`;
ctx.fillRect(0, 0, w, h);
}
ctx.restore();
}
function renderAt(t) {
// Terrain and lighting are static: cache their shadow map, not a moving world.
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true;
const projectedPin = new THREE.Vector3();
function renderAt(time) {
const t = banner ? ((time % 3) + 3) % 3 : Math.max(0, Math.min(30, time));
const loop = (t / 3) * Math.PI * 2;
let azimuth, distance, elevation, lookY;
if (banner) {
azimuth = 0.64 + Math.sin(loop) * 0.035;
distance = 89 + Math.cos(loop) * 1.1;
elevation = 40;
lookY = 0;
} else if (t < 4) {
const u = smooth(t / 4);
azimuth = 0.55 + u * 0.2;
distance = 99 - u * 13;
elevation = 48 - u * 5;
lookY = 0;
} else if (t < 9) {
const u = (t - 4) / 5;
azimuth = 0.45 + u * 0.6;
distance = 82 - u * 5;
elevation = 35;
lookY = 2;
} else if (t < 14) {
const u = (t - 9) / 5;
azimuth = 1.8 + u * 0.45;
distance = 64 + u * 4;
elevation = 26;
lookY = 3;
} else if (t < 19) {
const u = (t - 14) / 5;
azimuth = 0.6 + u * 0.35;
distance = 67;
elevation = 70 - u * 10;
lookY = 0;
} else if (t < 24) {
const u = (t - 19) / 5;
azimuth = 2.9 + u * 0.55;
distance = 83;
elevation = 35 + u * 7;
lookY = 0;
} else {
const u = (t - 24) / 6;
azimuth = 0.68 + u * 0.12;
distance = 93 - u * 4;
elevation = 44;
lookY = 0;
}
const travel = banner
? (1 - Math.cos(loop)) / 2
: easeInOut(Math.min(t, 3) / 3);
const azimuth = banner ? 0.66 + 0.045 * Math.sin(loop) : 0.56 + 0.13 * travel;
const distance = banner ? 79 + Math.sin(loop) * 1.6 : 100 - 13 * travel;
camera.position.set(
Math.sin(azimuth) * distance,
elevation,
banner ? 38 : 43,
Math.cos(azimuth) * distance,
);
camera.lookAt(0, lookY, 0);
// Move the landscape to the right to leave genuinely quiet space for titles.
camera.lookAt(0, 0, 0);
camera.setViewOffset(
width,
height,
-width * (banner ? 0.21 : 0.16),
-width * (banner ? 0.23 : 0.17),
0,
width,
height,
);
world.position.y = Math.sin(banner ? loop : t * 0.5) * 0.2;
world.position.y = 0;
pins.forEach((p, i) => {
p.group.position.y =
p.base + Math.sin((banner ? loop : t * 1.6) + i * 2) * 0.24;
p.group.rotation.y = (banner ? loop : t * 0.4) + i;
p.halo.scale.setScalar(1 + Math.sin((banner ? loop : t * 2) + i) * 0.08);
p.group.visible = banner ? i === 0 : t > 13.8 + i * 2;
p.base +
(banner
? Math.sin(loop) * 0.1
: (1 - enter(t, 23 + i * 0.08, 0.25)) * 1.5);
p.group.rotation.y = banner ? Math.sin(loop) * 0.12 : 0.15;
p.halo.scale.setScalar(1 + (banner ? (1 - Math.cos(loop)) * 0.035 : 0));
p.group.visible = i === 0 && (banner || t < 3 || t >= 23);
});
motes.rotation.y = banner ? Math.sin(loop) * 0.005 : t * 0.005;
ringGroup.rotation.y = banner ? loop : t * 0.25;
motes.rotation.y = banner ? Math.sin(loop) * 0.006 : 0;
ringGroup.rotation.y = banner ? loop : t * 0.12;
renderer.render(scene, camera);
overlay(t);
projectedPin.copy(pins[0].group.position);
projectedPin.y += 3;
projectedPin.project(camera);
composeFrame(ctx, renderer.domElement, {
width,
height,
banner,
time: t,
screenshots,
pin: {
x: (projectedPin.x * 0.5 + 0.5) * width,
y: (-projectedPin.y * 0.5 + 0.5) * height,
},
});
}
window.renderCinematic = renderAt;
window.cinematicCanvas = canvas;