← back to Animals

public/dogpark/intro.html

227 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Heading to the dog park…</title>
<style>
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{width:100%;height:100%;overflow:hidden;background:#7ec870;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#fff}
  canvas{display:block}
  #title{position:fixed;top:6%;left:0;right:0;text-align:center;font-size:30px;font-weight:300;letter-spacing:.04em;text-shadow:0 2px 6px rgba(0,0,0,.35);z-index:5;opacity:0;transition:opacity 600ms ease}
  #title.show{opacity:1}
  #subtitle{position:fixed;top:calc(6% + 46px);left:0;right:0;text-align:center;font-size:14px;color:rgba(255,255,255,.85);letter-spacing:.18em;text-transform:uppercase;z-index:5;opacity:0;transition:opacity 600ms ease 200ms}
  #subtitle.show{opacity:1}
  #fade{position:fixed;inset:0;background:#fff;opacity:0;pointer-events:none;z-index:50;transition:opacity 600ms ease}
  #fade.show{opacity:1}
  #skip{position:fixed;bottom:14px;right:14px;background:rgba(0,0,0,.4);color:#fff;border:0;padding:8px 14px;border-radius:8px;cursor:pointer;font-size:12px;z-index:5;opacity:.7}
  #skip:hover{opacity:1}
</style>
</head>
<body>
<div id="title">Welcome to the park</div>
<div id="subtitle"></div>
<button id="skip">skip →</button>
<div id="fade"></div>

<script type="importmap">
{ "imports": {
  "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
  "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
} }
</script>
<script type="module">
import * as THREE from 'three';

// ── Pull selection from localStorage (set by avatar picker page) ───────────
const avatarUrl = localStorage.getItem('dogpark_avatar_url');
const dogName   = localStorage.getItem('dogpark_dog_name')   || 'Buddy';
const dogColor  = localStorage.getItem('dogpark_dog_color')  || '#c97a3e';
const breedName = localStorage.getItem('dogpark_breed_name') || '';

document.getElementById('subtitle').textContent =
  breedName ? `${dogName} · ${breedName}` : dogName;
setTimeout(() => {
  document.getElementById('title').classList.add('show');
  document.getElementById('subtitle').classList.add('show');
}, 100);

// ── Three.js scene ─────────────────────────────────────────────────────────
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x9bd6f0);
scene.fog = new THREE.Fog(0x9bd6f0, 30, 80);

const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 200);
camera.position.set(0, 4, 12);
camera.lookAt(0, 1.2, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

// Lights
scene.add(new THREE.HemisphereLight(0xfff5e0, 0x4d8a3a, 0.8));
const sun = new THREE.DirectionalLight(0xfff0d0, 1.0);
sun.position.set(8, 20, 6);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -20; sun.shadow.camera.right = 20;
sun.shadow.camera.top = 20; sun.shadow.camera.bottom = -20;
scene.add(sun);

// Ground (matches the dog-park grass tone)
const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(120, 120),
  new THREE.MeshLambertMaterial({ color: 0x7ec870 }));
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);

// Path strip (subtle lighter green) so motion reads
const path = new THREE.Mesh(
  new THREE.PlaneGeometry(120, 3.5),
  new THREE.MeshLambertMaterial({ color: 0x96d480, transparent: true, opacity: 0.6 }));
path.rotation.x = -Math.PI / 2;
path.position.y = 0.01;
scene.add(path);

// A few decorative trees so there's depth cue
function tree(x, z, scale = 1) {
  const trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.18 * scale, 0.22 * scale, 1.2 * scale, 8),
    new THREE.MeshLambertMaterial({ color: 0x6b4423 }));
  trunk.position.set(x, 0.6 * scale, z);
  trunk.castShadow = true;
  scene.add(trunk);
  const top = new THREE.Mesh(
    new THREE.SphereGeometry(1.1 * scale, 12, 10),
    new THREE.MeshLambertMaterial({ color: 0x4d8a3a }));
  top.position.set(x, 1.9 * scale, z);
  top.castShadow = true;
  scene.add(top);
}
[ [-12, -8, 1.1], [10, -10, 0.9], [-15, 6, 1.2], [16, 4, 1.0],
  [-7, -14, 0.85], [14, -16, 1.05] ].forEach(t => tree(t[0], t[1], t[2]));

// ── Dog figure (low-poly, colored with dogColor) ───────────────────────────
const dog = new THREE.Group();
const dogMat = new THREE.MeshLambertMaterial({ color: new THREE.Color(dogColor) });

const body = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.65, 0.7), dogMat);
body.position.y = 0.55; body.castShadow = true; dog.add(body);

const head = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.55, 0.55), dogMat);
head.position.set(0.85, 0.85, 0); head.castShadow = true; dog.add(head);

const snout = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.28, 0.32),
  new THREE.MeshLambertMaterial({ color: 0x222 }));
snout.position.set(1.18, 0.78, 0); snout.castShadow = true; dog.add(snout);

const ear1 = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.28, 0.16), dogMat);
ear1.position.set(0.78, 1.18, 0.18); dog.add(ear1);
const ear2 = ear1.clone(); ear2.position.z = -0.18; dog.add(ear2);

const tail = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.12, 0.12), dogMat);
tail.position.set(-0.85, 0.7, 0); tail.rotation.z = 0.4; dog.add(tail);

// 4 legs — references kept for the run cycle
const legGeo = new THREE.BoxGeometry(0.18, 0.55, 0.18);
const legs = [];
for (const [x, z] of [[0.45, 0.25], [0.45, -0.25], [-0.45, 0.25], [-0.45, -0.25]]) {
  const leg = new THREE.Mesh(legGeo, dogMat);
  leg.position.set(x, 0.27, z);
  leg.castShadow = true;
  dog.add(leg);
  legs.push(leg);
}

dog.position.set(-12, 0, 0);
scene.add(dog);

// ── Avatar billboard floating above dog (uses chosen avatar image) ─────────
let avatarSprite = null;
if (avatarUrl) {
  const tex = new THREE.TextureLoader().load(avatarUrl,
    () => {},
    undefined,
    () => { avatarSprite && (avatarSprite.visible = false); });
  tex.colorSpace = THREE.SRGBColorSpace;
  const mat = new THREE.SpriteMaterial({ map: tex });
  avatarSprite = new THREE.Sprite(mat);
  avatarSprite.scale.set(1.4, 1.4, 1.4);
  avatarSprite.position.set(0, 2.3, 0);
  dog.add(avatarSprite);
}

// ── Animation loop: dog runs left → right, ears bounce, legs cycle ─────────
const startMs = performance.now();
const RUN_SECONDS = 5;
const TARGET_X = 12;

function tick(now) {
  const elapsed = (now - startMs) / 1000;
  const t = Math.min(1, elapsed / RUN_SECONDS);

  // Ease-out the run so the dog decelerates as it approaches the park entrance
  const ease = 1 - Math.pow(1 - t, 2.5);
  dog.position.x = -12 + (TARGET_X - (-12)) * ease;

  // Cute hop while running — disappears as we slow down
  const hopFreq = 7.0;
  const hopAmp  = 0.18 * (1 - t);
  dog.position.y = Math.max(0, Math.sin(elapsed * hopFreq) * hopAmp);

  // Leg cycle (front legs and back legs alternate)
  const phase = elapsed * 12;
  legs[0].rotation.x = Math.sin(phase) * 0.6;       // FR
  legs[1].rotation.x = Math.sin(phase + Math.PI) * 0.6; // FL
  legs[2].rotation.x = Math.sin(phase + Math.PI) * 0.6; // BR
  legs[3].rotation.x = Math.sin(phase) * 0.6;       // BL

  // Tail wag
  tail.rotation.y = Math.sin(elapsed * 14) * 0.5;

  // Body lean
  body.rotation.z = Math.sin(elapsed * hopFreq) * 0.04 * (1 - t);

  // Camera tracks the dog with a slow dolly-in
  const camX = dog.position.x * 0.6;
  camera.position.x = camX;
  camera.position.z = 12 - t * 4;
  camera.position.y = 4 - t * 0.8;
  camera.lookAt(dog.position.x, 1.2, 0);

  renderer.render(scene, camera);
  if (elapsed < RUN_SECONDS + 0.6) {
    requestAnimationFrame(tick);
  } else {
    enterPark();
  }
}
requestAnimationFrame(tick);

// ── Transition to the actual park ──────────────────────────────────────────
let entered = false;
function enterPark() {
  if (entered) return;
  entered = true;
  document.getElementById('fade').classList.add('show');
  setTimeout(() => { window.location.href = '/dog-park'; }, 650);
}
document.getElementById('skip').addEventListener('click', enterPark);

// Hard ceiling — never get stuck on the intro
setTimeout(enterPark, RUN_SECONDS * 1000 + 1500);
</script>
</body>
</html>