<!doctype html>
<title>Sistema Solar 3D - Juego (Amor)</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #05060a; }
#app { width: 100%; height: 100%; position: relative; }
canvas { display: block; }
.hud{
position: fixed; left: 12px; top: 12px;
padding: 10px 12px;
background: rgba(0,0,0,0.35);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
color: rgba(255,255,255,0.85);
font: 14px/1.25 system-ui, -apple-system, Segoe UI, Roboto, Arial;
user-select: none;
max-width: 360px;
backdrop-filter: blur(6px);
}
.hud b{ color: rgba(255,255,255,0.95); }
.hud .row{ margin-top: 6px; opacity: 0.95; }
.hint{
position: fixed; left: 12px; bottom: 12px;
color: rgba(255,255,255,.72);
font: 13px/1.25 system-ui, -apple-system, Segoe UI, Roboto, Arial;
user-select: none;
background: rgba(0,0,0,0.25);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 12px;
padding: 8px 10px;
backdrop-filter: blur(6px);
}
.btn{
display: inline-block;
margin-top: 8px;
padding: 6px 10px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,70,120,0.12);
color: rgba(255,255,255,0.92);
cursor: pointer;
}
.btn:active{ transform: translateY(1px); }
.centerMsg{
position: fixed; inset: 0;
display: grid; place-items: center;
pointer-events: none;
opacity: 0;
transition: opacity 220ms ease;
}
.centerMsg.show{ opacity: 1; }
.centerMsg .box{
pointer-events: none;
padding: 14px 16px;
border-radius: 16px;
background: rgba(0,0,0,0.35);
border: 1px solid rgba(255,255,255,0.14);
color: rgba(255,255,255,0.9);
font: 16px/1.25 system-ui, -apple-system, Segoe UI, Roboto, Arial;
backdrop-filter: blur(8px);
text-align: center;
max-width: 520px;
}
</style>
Controles
🖱️ Arrastra = rotar • Rueda = zoom • Doble click = reset
⌨️ W A S D moverte • Espacio subir • Shift bajar
🖱️ Click = “pulse” del sol ✨
Modo vuelo: ON
Tip: acércate a María ✨ para verla gigante 💖
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js";
import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js";
// ======= CONFIG =======
const RING_RADIUS = 110;
const RING_TILT_X = 0.45;
const RING_TILT_Z = 0.15;
const TEXT_NEON = "rgba(255,70,120,0.98)";
const TEXT_GLOW = "rgba(255,70,120,0.85)";
// “Gameplay”
let flyMode = true;
const MOVE_SPEED = 1.5; // velocidad base
const MOVE_SPEED_FAST = 3.0; // con Alt
const DAMP = 0.88;
// ======= Setup =======
const container = document.getElementById("app");
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 3000);
const defaultCamPos = new THREE.Vector3(0, 90, 260);
const defaultTarget = new THREE.Vector3(0, 25, 0);
camera.position.copy(defaultCamPos);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 80;
controls.maxDistance = 900;
controls.target.copy(defaultTarget);
window.addEventListener("dblclick", () => {
camera.position.copy(defaultCamPos);
controls.target.copy(defaultTarget);
controls.update();
pulseSol(1.0);
showCenterMsg("Siempre contigo 💫");
});
// ======= HUD =======
const toggleFlyBtn = document.getElementById("toggleFly");
toggleFlyBtn.addEventListener("click", () => {
flyMode = !flyMode;
toggleFlyBtn.textContent = `Modo vuelo: ${flyMode ? "ON" : "OFF"}`;
showCenterMsg(flyMode ? "Modo vuelo ACTIVADO
✈️" : "Modo vuelo DESACTIVADO 💤");
});
const centerMsg = document.getElementById("centerMsg");
const centerMsgBox = document.getElementById("centerMsgBox");
let msgTimer = null;
function showCenterMsg(text){
centerMsgBox.textContent = text;
centerMsg.classList.add("show");
clearTimeout(msgTimer);
msgTimer = setTimeout(() => centerMsg.classList.remove("show"), 900);
}
showCenterMsg("Siempre contigo 💫");
// ======= Luces =======
scene.add(new THREE.AmbientLight(0xffffff, 0.18));
const sunLight = new THREE.PointLight(0xffffff, 2.7, 1500);
sunLight.position.set(0, 0, 0);
scene.add(sunLight);
const fillLight = new THREE.DirectionalLight(0xffffff, 0.35);
fillLight.position.set(120, 140, 120);
scene.add(fillLight);
// ======= Estrellas =======
function makeStars(count = 1600) {
const geom = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const spread = 1400;
for (let i = 0; i < count; i++) {
const i3 = i * 3;
positions[i3 + 0] = (Math.random() - 0.5) * spread;
positions[i3 + 1] = (Math.random() - 0.5) * spread;
positions[i3 + 2] = (Math.random() - 0.5) * spread;
}
geom.setAttribute("position", new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xffffff,
size: 1.6,
sizeAttenuation: true,
transparent: true,
opacity: 0.65
});
const points = new THREE.Points(geom, mat);
scene.add(points);
return points;
}
const stars = makeStars();
// ======= Helpers visuales =======
function glowSprite(size = 210, opacity = 0.9, tint = [255,255,255]) {
const c = document.createElement("canvas");
c.width = 256; c.height = 256;
const g = c.getContext("2d");
const [r, gg, b] = tint;
const grd = g.createRadialGradient(128, 128, 8, 128, 128, 128);
grd.addColorStop(0.00, `rgba(${r},${gg},${b},${opacity})`);
grd.addColorStop(0.22, `rgba(${r},${gg},${b},${opacity * 0.55})`);
grd.addColorStop(1.00, "rgba(255,255,255,0)");
g.fillStyle = grd;
g.beginPath();
g.arc(128, 128, 128, 0, Math.PI * 2);
g.fill();
const tex = new THREE.CanvasTexture(c);
const spr = new THREE.Sprite(new THREE.SpriteMaterial({
map: tex,
transparent: true,
depthWrite: false
}));
spr.scale.set(size, size, 1);
return spr;
}
function neonLineMaterial(opacity = 0.25) {
return new THREE.LineBasicMaterial({
color: 0xff4a7d,
transparent: true,
opacity
});
}
function addRing(radius, tiltX, tiltZ, opacity = 0.22, segments = 220) {
const curve = new THREE.EllipseCurve(0, 0, radius * 1.08, radius * 0.86, 0, Math.PI * 2, false, 0);
const pts2 = curve.getPoints(segments);
const pts3 = pts2.map(p => new THREE.Vector3(p.x, 0, p.y));
const geom = new THREE.BufferGeometry().setFromPoints(pts3);
const line = new THREE.LineLoop(geom, neonLineMaterial(opacity));
line.rotation.x = tiltX;
line.rotation.z = tiltZ;
scene.add(line);
return line;
}
function textSprite(text, fontSize = 54, color = TEXT_NEON, glow = TEXT_GLOW) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const pad = 48;
ctx.font = `800 ${fontSize}px system-ui, -apple-system, Segoe UI, Roboto, Arial`;
const metrics = ctx.measureText(text);
const w = Math.ceil(metrics.width + pad * 2);
const h = Math.ceil(fontSize + pad * 2);
canvas.width = w;
canvas.height = h;
ctx.font = `800 ${fontSize}px system-ui, -apple-system, Segoe UI, Roboto, Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowColor = glow;
ctx.shadowBlur = 26;
ctx.strokeStyle = "rgba(255,120,170,0.35)";
ctx.lineWidth = 6;
ctx.strokeText(text, w / 2, h / 2);
ctx.fillStyle = color;
ctx.fillText(text, w / 2, h / 2);
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
const mat = new THREE.SpriteMaterial({
map: tex,
transparent: true,
depthWrite: false
});
const spr = new THREE.Sprite(mat);
const scale = 0.18;
spr.scale.set(w * scale, h * scale, 1);
return spr;
}
function makeGlassPlanet(radius = 10, tint = 0xff7aa7) {
const geom = new THREE.SphereGeometry(radius, 32, 32);
const mat = new THREE.MeshPhysicalMaterial({
color: tint,
transparent: true,
opacity: 0.12,
roughness: 0.05,
metalness: 0.0,
transmission: 1.0,
thickness: 0.7,
clearcoat: 1.0,
clearcoatRoughness: 0.15,
emissive: tint,
emissiveIntensity: 0.08
});
return new THREE.Mesh(geom, mat);
}
// ======= Sol blanco + glow =======
const sunGroup = new THREE.Group();
scene.add(sunGroup);
const sun = new THREE.Mesh(
new THREE.SphereGeometry(22, 40, 40),
new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 0.9,
roughness: 0.2,
metalness: 0.0
})
);
sunGroup.add(sun);
const sunGlow = glowSprite(240, 0.95, [255,255,255]);
sunGroup.add(sunGlow);
const sunNeonGlow = glowSprite(290, 0.45, [255, 70, 120]);
sunGroup.add(sunNeonGlow);
// “pulse” al click
let solPulse = 0;
function pulseSol(amount = 1.0){
solPulse = Math.min(1.5, solPulse + amount);
}
window.addEventListener("click", () => pulseSol(0.55));
// ======= Anillo Saturno =======
const mainRing = addRing(RING_RADIUS, RING_TILT_X, RING_TILT_Z, 0.22);
addRing(RING_RADIUS + 10, RING_TILT_X, RING_TILT_Z, 0.12);
addRing(RING_RADIUS - 12, RING_TILT_X, RING_TILT_Z, 0.10);
// ======= Frases (todas en el mismo anillo) =======
const phrases = [
"te amo",
"te quiero",
"siempre quiero estar contigo",
"mi bebe",
"mi chula",
"mi amor"
];
const ringGroup = new THREE.Group();
ringGroup.rotation.x = RING_TILT_X;
ringGroup.rotation.z = RING_TILT_Z;
scene.add(ringGroup);
const bodies = phrases.map((txt, i) => {
const group = new THREE.Group();
ringGroup.add(group);
const sprite = textSprite(txt, 54 - Math.min(i, 3) * 6);
group.add(sprite);
const planetSize = 9 + Math.min(txt.length, 28) * 0.25;
const planet = makeGlassPlanet(planetSize);
group.add(planet);
const halo = glowSprite(planetSize * 9.5, 0.35, [255, 70, 120]);
group.add(halo);
return {
group, sprite, planet, halo,
angle: (i / phrases.length) * Math.PI * 2,
speed: 0.0045 + i * 0.0006,
bobAmp: 3.5 + i * 0.35,
radius: RING_RADIUS,
phase: Math.random() * Math.PI * 2
};
});
// ======= MARÍA especial =======
const MARIA_RADIUS = 170;
addRing(MARIA_RADIUS, 0.15, -0.20, 0.28);
addRing(MARIA_RADIUS + 9, 0.15, -0.20, 0.14);
const maria = {
group: new THREE.Group(),
angle: Math.random() * Math.PI * 2,
speed: 0.0032,
radius: MARIA_RADIUS,
tiltX: 0.15,
tiltZ: -0.20,
bobAmp: 6.0
};
scene.add(maria.group);
const mariaSprite = textSprite("María ✨", 78, "rgba(255,120,180,1)", "rgba(255,120,180,0.95)");
maria.group.add(mariaSprite);
const mariaPlanet = makeGlassPlanet(22, 0xff9ac2);
maria.group.add(mariaPlanet);
const mariaHalo = glowSprite(320, 0.55, [255, 120, 180]);
maria.group.add(mariaHalo);
// ======= Controles tipo juego (WASD) =======
const keys = new Set();
window.addEventListener("keydown", (e) => {
keys.add(e.code);
if (e.code === "KeyF") { // toggle rápido
flyMode = !flyMode;
toggleFlyBtn.textContent = `Modo vuelo: ${flyMode ? "ON" : "OFF"}`;
showCenterMsg(flyMode ? "Modo vuelo ACTIVADO
✈️" : "Modo vuelo DESACTIVADO 💤");
}
});
window.addEventListener("keyup", (e) => keys.delete(e.code));
let vel = new THREE.Vector3(0,0,0);
function updateFly(dt){
if (!flyMode) return;
// dirección basada en cámara
const forward = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.normalize();
const right = new THREE.Vector3().crossVectors(forward, camera.up).normalize();
const up = new THREE.Vector3(0,1,0);
let speed = keys.has("AltLeft") || keys.has("AltRight") ? MOVE_SPEED_FAST : MOVE_SPEED;
const acc = new THREE.Vector3(0,0,0);
if (keys.has("KeyW")) acc.add(forward);
if (keys.has("KeyS")) acc.sub(forward);
if (keys.has("KeyD")) acc.add(right);
if (keys.has("KeyA")) acc.sub(right);
if (keys.has("Space")) acc.add(up);
if (keys.has("ShiftLeft") || keys.has("ShiftRight")) acc.sub(up);
if (acc.lengthSq() > 0) acc.normalize().multiplyScalar(speed);
// suavizado
vel.multiplyScalar(DAMP);
vel.addScaledVector(acc, dt * 60);
camera.position.addScaledVector(vel, dt * 60);
// mueve el target junto para que orbit controls no “jale” raro
controls.target.addScaledVector(vel, dt * 60);
}
// ======= Resize =======
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ======= Animación =======
let t = 0;
let last = performance.now();
function animate(now) {
requestAnimationFrame(animate);
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
t++;
updateFly(dt);
stars.rotation.y += 0.0007;
// pulse del sol
solPulse *= 0.92;
const pulse = 1 + Math.sin(t * 0.02) * 0.035 + solPulse * 0.10;
sunGlow.scale.set(240 * pulse, 240 * pulse, 1);
sunNeonGlow.scale.set(290 * (1 + Math.sin(t * 0.018) * 0.03 + solPulse * 0.08),
290 * (1 + Math.sin(t * 0.018) * 0.03 + solPulse * 0.08), 1);
mainRing.material.opacity = 0.20 + Math.sin(t * 0.01) * 0.02;
for (const b of bodies) {
b.angle += b.speed;
const x = Math.cos(b.angle) * b.radius * 1.08;
const z = Math.sin(b.angle) * b.radius * 0.86;
const y = Math.sin(b.angle * 2.0 + b.phase) * b.bobAmp;
b.group.position.set(x, y, z);
const hp = 1 + Math.sin(t * 0.03 + b.phase) * 0.05;
const base = b.planet.geometry.parameters.radius * 9.5;
b.halo.scale.set(base * hp, base * hp, 1);
b.sprite.quaternion.copy(camera.quaternion);
}
maria.angle += maria.speed;
const mx = Math.cos(maria.angle) * maria.radius * 1.06;
const mz = Math.sin(maria.angle) * maria.radius * 0.84;
const my = Math.sin(maria.angle * 1.4) * maria.bobAmp;
const mv = new THREE.Vector3(mx, my, mz);
mv.applyAxisAngle(new THREE.Vector3(1, 0, 0), maria.tiltX);
mv.applyAxisAngle(new THREE.Vector3(0, 0, 1), maria.tiltZ);
maria.group.position.copy(mv);
const mariaPulse = 1 + Math.sin(t * 0.03) * 0.06 + solPulse * 0.06;
mariaHalo.scale.set(320 * mariaPulse, 320 * mariaPulse, 1);
mariaSprite.quaternion.copy(camera.quaternion);
// si te acercas a María, mensaje romántico
const distToMaria = camera.position.distanceTo(maria.group.position);
if (distToMaria < 95) {
showCenterMsg("Te amo, María 💖");
}
controls.update();
renderer.render(scene, camera);
}
animate(performance.now());
</script>