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); }