← 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>