music-video-gen/tv-player/mockups/guineapigs.html
2025-11-09 08:46:05 +01:00

413 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Guinea Pig Garden</title>
<!-- Load Tailwind CSS for modern styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Load Three.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* Custom CSS to ensure the canvas fills the viewport */
body {
margin: 0;
overflow: hidden;
font-family: 'Inter', sans-serif;
color: #333;
}
canvas {
display: block;
}
#info-box {
position: absolute;
top: 10px;
left: 10px;
padding: 10px 15px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10;
border-left: 4px solid #38A169; /* Garden color accent */
}
</style>
</head>
<body>
<div id="container"></div>
<div id="info-box">
<h2 class="font-bold text-lg mb-1">Guinea Pig Garden Frolic</h2>
<p class="text-sm">Click and drag to look around the field.</p>
</div>
<script>
// Global variables for Three.js
let scene, camera, renderer, controls;
const guineaPigs = [];
const NUM_PIGS = 80;
const ROOM_SIZE = 15; // Defines the half-width/depth of the movement area (now a field)
// --- Utility Functions (TTS Helpers kept for completeness) ---
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
function pcmToWav(int16Array, sampleRate) {
const numChannels = 1;
const bytesPerSample = 2;
const blockAlign = numChannels * bytesPerSample;
const dataSize = int16Array.length * bytesPerSample;
const buffer = new ArrayBuffer(44 + dataSize);
const view = new DataView(buffer);
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + dataSize, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * blockAlign, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, dataSize, true);
for (let i = 0; i < int16Array.length; i++) {
view.setInt16(44 + i * 2, int16Array[i], true);
}
return new Blob([buffer], { type: 'audio/wav' });
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
}
// --- Three.js Setup and Geometry ---
function init() {
// 1. Scene Setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // Lighter Sky Blue
// 2. Camera Setup
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 20);
// 3. Renderer Setup
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; // Enable shadows
document.getElementById('container').appendChild(renderer.domElement);
// 4. Lighting (Sunlight simulation)
const ambientLight = new THREE.AmbientLight(0x404040, 1.5); // Soft white light
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5); // Brighter sun
directionalLight.position.set(5, 20, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -ROOM_SIZE * 2;
directionalLight.shadow.camera.right = ROOM_SIZE * 2;
directionalLight.shadow.camera.top = ROOM_SIZE * 2;
directionalLight.shadow.camera.bottom = -ROOM_SIZE * 2;
scene.add(directionalLight);
// 5. Garden Setup
createGarden();
// 6. Guinea Pigs
createGuineaPigs();
// 7. Camera Controls (using custom mouse drag controls)
setupControls();
// Event Listeners
window.addEventListener('resize', onWindowResize, false);
}
/**
* Creates a simple, low-poly geometry for a single guinea pig.
* The pig is returned as a THREE.Group.
*/
function createGuineaPigMesh() {
const pigGroup = new THREE.Group();
// Body (Capsule/Cylinder)
const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.6, 1.5, 8);
const color = new THREE.Color(Math.random(), Math.random(), Math.random()).lerp(new THREE.Color(0.6, 0.6, 0.6), 0.5); // Mixed fur colors
const bodyMaterial = new THREE.MeshLambertMaterial({ color: color });
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.rotation.z = Math.PI / 2; // Lay it flat along the X-axis
body.position.y = 0.3; // Lift off floor
body.castShadow = true;
body.receiveShadow = true;
pigGroup.add(body);
// Head (Sphere)
const headGeometry = new THREE.SphereGeometry(0.4, 8, 8);
const headMaterial = new THREE.MeshLambertMaterial({ color: color.clone().offsetHSL(0, 0, 0.1) });
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.set(0.8, 0.3, 0); // Head positioned along the positive X-axis
head.castShadow = true;
pigGroup.add(head);
// Ears (Cones)
const earGeometry = new THREE.ConeGeometry(0.1, 0.2, 4);
const earMaterial = new THREE.MeshLambertMaterial({ color: 0xcb997e }); // Pinkish
const earL = new THREE.Mesh(earGeometry, earMaterial);
earL.position.set(0.8, 0.5, 0.3);
earL.rotation.x = Math.PI / 2;
earL.rotation.y = -Math.PI / 8;
earL.rotation.z = -Math.PI / 4;
pigGroup.add(earL);
const earR = new THREE.Mesh(earGeometry, earMaterial);
earR.position.set(0.8, 0.5, -0.3);
earR.rotation.x = Math.PI / 2;
earR.rotation.y = Math.PI / 8;
earR.rotation.z = Math.PI / 4;
pigGroup.add(earR);
// FIX: Rotate the entire pig group -90 degrees around the Y-axis.
// This aligns the pig's "forward" direction (originally +X) with the scene's +Z axis,
// which correctly matches the orientation calculated by the movement logic.
pigGroup.rotation.y = -Math.PI / 2;
return pigGroup;
}
/**
* Creates and adds NUM_PIGS instances to the scene with initial random positions.
*/
function createGuineaPigs() {
for (let i = 0; i < NUM_PIGS; i++) {
const pigMesh = createGuineaPigMesh();
// Initial random position within the room bounds
pigMesh.position.x = (Math.random() - 0.5) * ROOM_SIZE * 1.8;
pigMesh.position.z = (Math.random() - 0.5) * ROOM_SIZE * 1.8;
pigMesh.rotation.y = Math.random() * Math.PI * 2; // Random initial direction
// Attach movement properties to the mesh
pigMesh.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.05,
0,
(Math.random() - 0.5) * 0.05
);
pigMesh.turnCooldown = Math.random() * 60 + 60; // Pigs turn every 60-120 frames
pigMesh.turnTimer = 0;
pigMesh.runAnimation = 0;
scene.add(pigMesh);
guineaPigs.push(pigMesh);
}
}
/**
* Adds randomized grass and flower meshes to the garden.
*/
function addFoliage() {
const FOLIAGE_COUNT = 300;
const MAX_BOUND = ROOM_SIZE * 1.8; // Area where foliage can spawn
// Simple Grass Mesh (tall, thin cylinder)
const grassGeometry = new THREE.CylinderGeometry(0.05, 0.05, 1, 4);
const grassMaterial = new THREE.MeshLambertMaterial({ color: 0x68a87b }); // Slightly different green
// Simple Flower Mesh (stem + petal sphere)
const stemGeometry = new THREE.CylinderGeometry(0.03, 0.03, 0.5, 4);
const petalGeometry = new THREE.SphereGeometry(0.15, 6, 6);
const flowerMaterials = [
new THREE.MeshLambertMaterial({ color: 0xffd700 }), // Yellow
new THREE.MeshLambertMaterial({ color: 0xff69b4 }), // Pink
new THREE.MeshLambertMaterial({ color: 0x7b68ee }) // Lavender
];
const stemMaterial = new THREE.MeshLambertMaterial({ color: 0x8fbc8f });
for (let i = 0; i < FOLIAGE_COUNT; i++) {
const x = (Math.random() - 0.5) * MAX_BOUND;
const z = (Math.random() - 0.5) * MAX_BOUND;
// Alternate between grass and flowers
if (i % 3 === 0) {
// Add Flower
const stem = new THREE.Mesh(stemGeometry, stemMaterial);
stem.position.set(x, 0.25, z);
stem.receiveShadow = true;
scene.add(stem);
const petals = new THREE.Mesh(petalGeometry, flowerMaterials[i % 3]);
petals.position.set(x, 0.5 + Math.random() * 0.1, z);
petals.castShadow = true;
scene.add(petals);
} else {
// Add Grass
const grass = new THREE.Mesh(grassGeometry, grassMaterial);
const height = Math.random() * 0.5 + 0.5;
grass.scale.y = height;
grass.position.set(x, height / 2, z);
grass.rotation.y = Math.random() * Math.PI * 2;
grass.receiveShadow = true;
scene.add(grass);
}
}
}
/**
* Creates the garden ground (replaces the room walls and floor).
*/
function createGarden() {
const grassMaterial = new THREE.MeshLambertMaterial({ color: 0x4f8e5b }); // Rich Grass Green
// Ground
const groundGeometry = new THREE.PlaneGeometry(ROOM_SIZE * 4, ROOM_SIZE * 4); // Large ground plane
const ground = new THREE.Mesh(groundGeometry, grassMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = 0;
ground.receiveShadow = true;
scene.add(ground);
// Add flowers and grass
addFoliage();
}
// --- Interaction / Controls ---
function setupControls() {
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
const rotationSpeed = 0.005;
// Create a target for the camera to look at, placed roughly in the center of the scene.
const pivot = new THREE.Group();
pivot.position.set(0, 0, 0);
scene.add(pivot);
pivot.add(camera);
const mouseDownHandler = (e) => {
isDragging = true;
previousMousePosition.x = e.clientX;
previousMousePosition.y = e.clientY;
};
const mouseUpHandler = () => {
isDragging = false;
};
const mouseMoveHandler = (e) => {
if (!isDragging) return;
const deltaX = e.clientX - previousMousePosition.x;
const deltaY = e.clientY - previousMousePosition.y;
// Rotate the pivot around the Y axis for horizontal movement
pivot.rotation.y += deltaX * rotationSpeed;
// Calculate vertical rotation (tilt the camera up/down)
let newXRotation = pivot.rotation.x + deltaY * rotationSpeed;
// Clamp the vertical rotation to prevent flipping
const PI_HALF = Math.PI / 2;
newXRotation = Math.max(-PI_HALF + 0.1, Math.min(PI_HALF - 0.1, newXRotation));
pivot.rotation.x = newXRotation;
previousMousePosition.x = e.clientX;
previousMousePosition.y = e.clientY;
};
renderer.domElement.addEventListener('mousedown', mouseDownHandler, false);
renderer.domElement.addEventListener('mouseup', mouseUpHandler, false);
renderer.domElement.addEventListener('mousemove', mouseMoveHandler, false);
renderer.domElement.addEventListener('mouseout', mouseUpHandler, false); // Stop dragging if mouse leaves
}
// --- Animation Loop ---
function updateGuineaPigs() {
const boundary = ROOM_SIZE * 0.9;
const speed = 0.05;
guineaPigs.forEach(pig => {
// 1. Movement and Rotation
pig.position.x += pig.velocity.x * speed;
pig.position.z += pig.velocity.z * speed;
// 2. Bound Checking (Check if near edges of the field)
let needsTurn = false;
if (pig.position.x > boundary || pig.position.x < -boundary) {
pig.velocity.x *= -1;
needsTurn = true;
pig.position.x = Math.max(-boundary, Math.min(boundary, pig.position.x));
}
if (pig.position.z > boundary || pig.position.z < -boundary) {
pig.velocity.z *= -1;
needsTurn = true;
pig.position.z = Math.max(-boundary, Math.min(boundary, pig.position.z));
}
// 3. Random Turning
pig.turnTimer++;
if (pig.turnTimer > pig.turnCooldown || needsTurn) {
pig.turnTimer = 0;
// Calculate new random velocity and rotation
pig.velocity.x = (Math.random() - 0.5) * 2;
pig.velocity.z = (Math.random() - 0.5) * 2;
pig.velocity.normalize(); // Keep consistent speed
// Set rotation based on new velocity direction (no offset needed as the mesh is pre-rotated)
pig.rotation.y = Math.atan2(pig.velocity.x, pig.velocity.z)-Math.PI/2;
pig.turnCooldown = Math.random() * 60 + 60;
}
// 4. Simple 'Run' Animation (wobbling)
pig.runAnimation += 0.1;
pig.rotation.x = Math.sin(pig.runAnimation) * 0.05; // Head wobble
pig.position.y = Math.abs(Math.sin(pig.runAnimation * 0.5)) * 0.05 + 0.3; // Slight bounce
});
}
function animate() {
requestAnimationFrame(animate);
updateGuineaPigs(); // Move and update the pigs
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Start the application when the window loads
window.onload = function () {
// Check if Three.js is loaded
if (typeof THREE === 'undefined') {
console.error("Three.js not loaded. Please ensure the CDN link is correct.");
return;
}
init();
animate();
};
</script>
</body>
</html>