Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m14s
634 lines
19 KiB
JavaScript
634 lines
19 KiB
JavaScript
import * as THREE from "three";
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
const banner = params.get("mode") === "banner";
|
|
const width = Number(params.get("width")) || (banner ? 1200 : 1280);
|
|
const height = Number(params.get("height")) || (banner ? 480 : 720);
|
|
const renderer = new THREE.WebGLRenderer({
|
|
antialias: true,
|
|
preserveDrawingBuffer: true,
|
|
});
|
|
renderer.setSize(width, height);
|
|
renderer.setPixelRatio(1);
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.35;
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color("#0c211c");
|
|
scene.fog = new THREE.FogExp2("#0c211c", 0.008);
|
|
const camera = new THREE.PerspectiveCamera(38, width / height, 0.1, 350);
|
|
scene.add(new THREE.HemisphereLight(0xc5e4d4, 0x263522, 2.1));
|
|
const sun = new THREE.DirectionalLight(0xffd8a2, 4.2);
|
|
sun.position.set(-24, 45, 22);
|
|
sun.castShadow = true;
|
|
sun.shadow.mapSize.set(2048, 2048);
|
|
sun.shadow.camera.left = -42;
|
|
sun.shadow.camera.right = 42;
|
|
sun.shadow.camera.top = 42;
|
|
sun.shadow.camera.bottom = -42;
|
|
sun.shadow.normalBias = 0.08;
|
|
scene.add(sun);
|
|
const rim = new THREE.DirectionalLight(0x74d6d3, 2.5);
|
|
rim.position.set(20, 8, -30);
|
|
scene.add(rim);
|
|
const world = new THREE.Group();
|
|
scene.add(world);
|
|
const palettes = {
|
|
rock: [0x4b655d, 0x546e62, 0x3c514c, 0x668177],
|
|
dirt: [0x66563c, 0x786548, 0x8a7150],
|
|
grass: [0x77a653, 0x90b962, 0x648f45, 0xa0bd6d],
|
|
sand: [0xceb98a, 0xe1cd9e, 0xb9ad80],
|
|
snow: [0xcddbd1, 0xe4eee1],
|
|
leaves: [0x3c7449, 0x4d925b, 0x79a456, 0x9fac61],
|
|
wood: [0x69513a, 0x84654a],
|
|
water: [0x3b9190, 0x56aeaa],
|
|
};
|
|
const instances = Object.fromEntries(
|
|
Object.keys(palettes).map((key) => [key, []]),
|
|
);
|
|
const hash = (x, z, salt = 0) => {
|
|
const n = Math.sin(x * 127.1 + z * 311.7 + salt * 74.7) * 43758.5453;
|
|
return n - Math.floor(n);
|
|
};
|
|
function block(kind, x, y, z, sx = 1, sy = 1, sz = 1, variation = 0) {
|
|
instances[kind].push({
|
|
x,
|
|
y,
|
|
z,
|
|
sx,
|
|
sy,
|
|
sz,
|
|
color:
|
|
palettes[kind][Math.floor(hash(x, z, variation) * palettes[kind].length)],
|
|
});
|
|
}
|
|
function groundHeight(x, z) {
|
|
const hill = Math.exp(-((x - 6) ** 2 + (z + 5) ** 2) / 49) * 10;
|
|
const hill2 = Math.exp(-((x + 8) ** 2 + (z - 5) ** 2) / 48) * 3;
|
|
return Math.floor(
|
|
2 + hill + hill2 + Math.sin(x * 0.32) * 0.8 + Math.cos(z * 0.38) * 0.7,
|
|
);
|
|
}
|
|
for (let x = -23; x <= 23; x++) {
|
|
for (let z = -19; z <= 19; z++) {
|
|
const edge = (x / 23) ** 2 + (z / 19) ** 2;
|
|
if (edge > 0.92 + Math.sin(x * 0.7 + z * 0.3) * 0.07) continue;
|
|
const river = Math.abs(z - (Math.sin(x * 0.16) * 3 + 3)) < 1.7;
|
|
const h = river ? 1 : groundHeight(x, z);
|
|
const depth = Math.max(1, Math.floor((1 - edge) * 8 + hash(x, z) * 2));
|
|
block("rock", x, -depth / 2, z, 0.99, depth, 0.99);
|
|
block("dirt", x, h / 2, z, 1, h, 1);
|
|
const kind = river
|
|
? "water"
|
|
: h > 9
|
|
? "snow"
|
|
: edge > 0.73
|
|
? "sand"
|
|
: "grass";
|
|
block(kind, x, h + 0.08, z, 1, 0.32, 1);
|
|
if (
|
|
!river &&
|
|
edge < 0.65 &&
|
|
h < 7 &&
|
|
hash(x, z, 7) > 0.958 &&
|
|
(x % 3 === 0 || z % 4 === 0)
|
|
) {
|
|
const tall = 3 + Math.floor(hash(x, z, 9) * 2);
|
|
block("wood", x, h + tall / 2, z, 0.55, tall, 0.55);
|
|
block("leaves", x, h + tall, z, 2.8, 1.8, 2.8);
|
|
block("leaves", x, h + tall + 1.1, z, 1.65, 1.2, 1.65, 2);
|
|
}
|
|
if (edge > 0.7 && edge < 0.91 && hash(x, z, 12) > 0.94)
|
|
block("leaves", x, -depth + 0.2, z, 0.24, 2 + hash(x, z, 10) * 3, 0.24);
|
|
}
|
|
}
|
|
// A warm-lit expedition hut and an old stone arch make the terrain recognizable.
|
|
const hutX = -8,
|
|
hutZ = -5,
|
|
hutY = groundHeight(hutX, hutZ) + 0.2;
|
|
block("wood", hutX, hutY + 1.15, hutZ, 3.2, 2.4, 3);
|
|
for (let i = 0; i < 3; i++)
|
|
block("rock", hutX, hutY + 2.5 + i * 0.4, hutZ, 4 - i, 0.5, 3.8);
|
|
for (const x of [9, 13]) for (let y = 0; y < 5; y++) block("rock", x, 4 + y, 8);
|
|
for (let x = 9; x <= 13; x++) block("rock", x, 9, 8);
|
|
const box = new THREE.BoxGeometry(1, 1, 1);
|
|
const dummy = new THREE.Object3D();
|
|
for (const [kind, cells] of Object.entries(instances)) {
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
roughness: kind === "water" ? 0.24 : 0.92,
|
|
metalness: kind === "water" ? 0.25 : 0.02,
|
|
});
|
|
const mesh = new THREE.InstancedMesh(box, material, cells.length);
|
|
cells.forEach((b, i) => {
|
|
dummy.position.set(b.x, b.y, b.z);
|
|
dummy.scale.set(b.sx, b.sy, b.sz);
|
|
dummy.updateMatrix();
|
|
mesh.setMatrixAt(i, dummy.matrix);
|
|
mesh.setColorAt(i, new THREE.Color(b.color));
|
|
});
|
|
mesh.instanceMatrix.needsUpdate = true;
|
|
mesh.instanceColor.needsUpdate = true;
|
|
mesh.castShadow = kind !== "water";
|
|
mesh.receiveShadow = true;
|
|
world.add(mesh);
|
|
}
|
|
const windowLight = new THREE.Mesh(
|
|
new THREE.BoxGeometry(0.12, 0.7, 0.7),
|
|
new THREE.MeshBasicMaterial({ color: 0xffce77 }),
|
|
);
|
|
windowLight.position.set(hutX - 1.64, hutY + 1.25, hutZ);
|
|
world.add(windowLight);
|
|
|
|
// Thin cartographic rings float beneath the cutaway, like a world waiting to be found.
|
|
const ringGroup = new THREE.Group();
|
|
ringGroup.position.y = -8;
|
|
for (const radius of [25, 29, 33]) {
|
|
const points = Array.from(
|
|
{ length: 161 },
|
|
(_, i) =>
|
|
new THREE.Vector3(
|
|
Math.cos((i / 160) * Math.PI * 2) * radius,
|
|
0,
|
|
Math.sin((i / 160) * Math.PI * 2) * radius,
|
|
),
|
|
);
|
|
const line = new THREE.LineLoop(
|
|
new THREE.BufferGeometry().setFromPoints(points),
|
|
new THREE.LineBasicMaterial({
|
|
color: 0x759b7b,
|
|
transparent: true,
|
|
opacity: 0.25,
|
|
}),
|
|
);
|
|
ringGroup.add(line);
|
|
}
|
|
world.add(ringGroup);
|
|
const sweepPoints = Array.from(
|
|
{ length: 49 },
|
|
(_, i) =>
|
|
new THREE.Vector3(
|
|
Math.cos((i / 48) * 0.7) * 29,
|
|
0.04,
|
|
Math.sin((i / 48) * 0.7) * 29,
|
|
),
|
|
);
|
|
ringGroup.add(
|
|
new THREE.Line(
|
|
new THREE.BufferGeometry().setFromPoints(sweepPoints),
|
|
new THREE.LineBasicMaterial({
|
|
color: 0xd6bb76,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
}),
|
|
),
|
|
);
|
|
|
|
function pin(color, x, z) {
|
|
const group = new THREE.Group();
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
color,
|
|
emissive: color,
|
|
emissiveIntensity: 0.25,
|
|
metalness: 0.35,
|
|
roughness: 0.3,
|
|
});
|
|
const head = new THREE.Mesh(new THREE.OctahedronGeometry(1.4), mat);
|
|
head.position.y = 3;
|
|
const stem = new THREE.Mesh(new THREE.ConeGeometry(0.35, 2.2, 4), mat);
|
|
stem.rotation.z = Math.PI;
|
|
stem.position.y = 1.25;
|
|
group.add(head, stem);
|
|
const halo = new THREE.Mesh(
|
|
new THREE.TorusGeometry(2, 0.055, 6, 64),
|
|
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.75 }),
|
|
);
|
|
halo.rotation.x = -Math.PI / 2;
|
|
halo.position.y = 0.15;
|
|
group.add(halo);
|
|
group.position.set(x, groundHeight(x, z) + 0.5, z);
|
|
world.add(group);
|
|
return { group, halo, base: group.position.y };
|
|
}
|
|
const pins = [
|
|
pin(0xffcf70, 5, -5),
|
|
pin(0xa3e271, -12, 9),
|
|
pin(0xef7967, 12, 5),
|
|
];
|
|
const moteGeo = new THREE.BufferGeometry();
|
|
const motePositions = [];
|
|
for (let i = 0; i < 150; i++)
|
|
motePositions.push(
|
|
(hash(i, 4) - 0.5) * 130,
|
|
hash(i, 5) * 58 - 12,
|
|
(hash(i, 6) - 0.5) * 100,
|
|
);
|
|
moteGeo.setAttribute(
|
|
"position",
|
|
new THREE.Float32BufferAttribute(motePositions, 3),
|
|
);
|
|
const motes = new THREE.Points(
|
|
moteGeo,
|
|
new THREE.PointsMaterial({
|
|
color: 0xdce8b9,
|
|
size: 0.13,
|
|
transparent: true,
|
|
opacity: 0.6,
|
|
}),
|
|
);
|
|
scene.add(motes);
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
document.body.prepend(canvas);
|
|
const ctx = canvas.getContext("2d");
|
|
await document.fonts.load("24px Pixel");
|
|
await document.fonts.ready;
|
|
const screenshots = banner
|
|
? {}
|
|
: Object.fromEntries(
|
|
await Promise.all(
|
|
["home", "catalog", "map", "game"].map(async (name) => {
|
|
const image = new Image();
|
|
image.src = `/shots/${name}.jpg`;
|
|
await image.decode();
|
|
return [name, image];
|
|
}),
|
|
),
|
|
);
|
|
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) {
|
|
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;
|
|
}
|
|
camera.position.set(
|
|
Math.sin(azimuth) * distance,
|
|
elevation,
|
|
Math.cos(azimuth) * distance,
|
|
);
|
|
camera.lookAt(0, lookY, 0);
|
|
// Move the landscape to the right to leave genuinely quiet space for titles.
|
|
camera.setViewOffset(
|
|
width,
|
|
height,
|
|
-width * (banner ? 0.21 : 0.16),
|
|
0,
|
|
width,
|
|
height,
|
|
);
|
|
world.position.y = Math.sin(banner ? loop : t * 0.5) * 0.2;
|
|
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;
|
|
});
|
|
motes.rotation.y = banner ? Math.sin(loop) * 0.005 : t * 0.005;
|
|
ringGroup.rotation.y = banner ? loop : t * 0.25;
|
|
renderer.render(scene, camera);
|
|
overlay(t);
|
|
}
|
|
window.renderCinematic = renderAt;
|
|
window.cinematicCanvas = canvas;
|
|
document.querySelector("#status").remove();
|
|
renderAt(Number(params.get("time")) || 0);
|
|
window.cinematicReady = true;
|
|
if (!params.has("still")) {
|
|
const start = performance.now();
|
|
function loopFrame(now) {
|
|
renderAt(((now - start) / 1000) % (banner ? 3 : 30));
|
|
requestAnimationFrame(loopFrame);
|
|
}
|
|
requestAnimationFrame(loopFrame);
|
|
}
|