MermaidsofMinnetonka / index.html
awacke1's picture
Update index.html
af7d9fa verified
<!DOCTYPE html>
<html lang="en">
<head>
<title>Mermaid of Minnetonka</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
font-family: 'Georgia', serif;
}
#blocker {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
#start-button {
padding: 20px 40px;
font-size: 24px;
background: #1a3a5a;
color: #cceeff;
border: 2px solid #cceeff;
border-radius: 10px;
cursor: pointer;
text-shadow: 0 0 10px #cceeff;
box-shadow: 0 0 20px rgba(135, 206, 250, 0.5);
}
#top-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 10px;
background: linear-gradient(to bottom, rgba(0, 15, 30, 0.8), rgba(0, 15, 30, 0));
box-sizing: border-box;
color: #e0f7ff;
text-shadow: 0 0 5px #66aaff;
z-index: 10;
}
#lyric-ticker-container {
width: 100%;
overflow: hidden;
white-space: nowrap;
}
#lyric-ticker-text {
display: inline-block;
padding-left: 100%;
animation: scrollText linear infinite;
}
@keyframes scrollText {
from { transform: translateX(0%); }
to { transform: translateX(-100%); }
}
#controls-ui {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 20px;
font-size: 14px;
}
#speed-control {
display: flex;
align-items: center;
gap: 10px;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 150px;
height: 5px;
background: rgba(135, 206, 250, 0.3);
border-radius: 5px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 15px;
height: 15px;
background: #87cefa;
cursor: pointer;
border-radius: 50%;
border: 2px solid #e0f7ff;
}
</style>
</head>
<body>
<div id="blocker">
<button id="start-button">Start Experience</button>
</div>
<div id="top-bar">
<div id="lyric-ticker-container">
<span id="lyric-ticker-text"></span>
</div>
<div id="controls-ui">
<span>W/S: Fwd/Back | A/D: Turn | Space: Ascend | Shift: Descend</span>
<div id="speed-control">
<label for="speed-slider">Scroll Speed:</label>
<input type="range" id="speed-slider" min="1" max="100" value="50">
</div>
</div>
</div>
<audio id="song" loop>
<source src="YOUR_SONG_FILE.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.165.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { Water } from 'three/addons/objects/Water.js';
import { Sky } from 'three/addons/objects/Sky.js';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
let scene, camera, renderer, mermaid, water, sky, terrain;
let controls = {};
const clock = new THREE.Clock();
const worldSize = 4000;
const waterLevel = 100;
let schools = [];
let animationId;
const mermaidVelocity = new THREE.Vector3();
const mermaidState = {
turnSpeed: 0,
forwardSpeed: 0,
tailSegments: [],
hairStrands: [],
leftEye: null,
rightEye: null
};
const raycaster = new THREE.Raycaster();
const downVector = new THREE.Vector3(0, -1, 0);
const rhymingWords = [
'gold', 'told', 'blue', 'through', 'see', 'be', 'stone', 'throne',
'haze', 'ways', 'blue', 'new', 'remains', 'wanes',
'art', 'apart', 'hymn', 'whim', 'before', 'shore', 'eulogy', 'monarchy',
'historian', 'pre-diluvian', 'time', 'crime', 'rise', 'skies', 'feel', 'real',
'haze', 'ways', 'blue', 'new', 'remains', 'wanes',
'know', 'flow', 'keep', 'deep', 'drowned', 'Mound'
].join(' • ');
// --- L-SYSTEM ENGINE ---
function generateLSystem(axiom, rules, iterations) {
let currentString = axiom;
for (let i = 0; i < iterations; i++) {
let nextString = '';
for (const char of currentString) {
nextString += rules[char] || char;
}
currentString = nextString;
}
return currentString;
}
function createLSystemGeometry(lsystemString, angle, length) {
const points = [];
const turtle = {
pos: new THREE.Vector3(0, 0, 0),
dir: new THREE.Vector3(0, 1, 0)
};
const stack = [];
points.push(turtle.pos.clone());
for (const char of lsystemString) {
switch (char) {
case 'F':
turtle.pos.addScaledVector(turtle.dir, length);
points.push(turtle.pos.clone());
break;
case '+':
turtle.dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), angle);
break;
case '-':
turtle.dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), -angle);
break;
case '&':
turtle.dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle);
break;
case '^':
turtle.dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), -angle);
break;
case '[':
stack.push({ pos: turtle.pos.clone(), dir: turtle.dir.clone() });
break;
case ']':
const popped = stack.pop();
turtle.pos = popped.pos;
turtle.dir = popped.dir;
points.push(turtle.pos.clone()); // Create a gap in the tube
points.push(turtle.pos.clone());
break;
}
}
const curve = new THREE.CatmullRomCurve3(points);
return new THREE.TubeGeometry(curve, Math.round(points.length * 1.5), 0.2, 5, false);
}
// --- BOIDS FLOCKING ENGINE ---
class Boid {
constructor(mesh) {
this.mesh = mesh;
this.velocity = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
this.maxSpeed = 10;
this.maxForce = 0.3;
}
update(boids, school) {
const separation = this.separate(boids);
const alignment = this.align(boids);
const cohesion = this.cohere(boids);
const avoidance = this.avoid(mermaid.position);
const wander = this.wander(school.target);
separation.multiplyScalar(2.0);
alignment.multiplyScalar(1.0);
cohesion.multiplyScalar(1.0);
avoidance.multiplyScalar(3.0);
wander.multiplyScalar(0.5);
this.velocity.add(separation).add(alignment).add(cohesion).add(avoidance).add(wander);
this.velocity.clampLength(0, this.maxSpeed);
this.mesh.position.addScaledVector(this.velocity, clock.getDelta());
this.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), this.velocity.clone().normalize());
}
wander(target) {
const desired = target.clone().sub(this.mesh.position);
desired.setLength(this.maxSpeed);
const steer = desired.sub(this.velocity);
steer.clampLength(0, this.maxForce);
return steer;
}
avoid(targetPos) {
const steer = new THREE.Vector3();
const distance = this.mesh.position.distanceTo(targetPos);
if (distance < 50) {
const desired = this.mesh.position.clone().sub(targetPos);
desired.setLength(this.maxSpeed);
steer.subVectors(desired, this.velocity).clampLength(0, this.maxForce * 2);
}
return steer;
}
separate(boids) {
const desiredSeparation = 10.0;
const steer = new THREE.Vector3();
let count = 0;
for (const other of boids) {
const d = this.mesh.position.distanceTo(other.mesh.position);
if ((d > 0) && (d < desiredSeparation)) {
const diff = new THREE.Vector3().subVectors(this.mesh.position, other.mesh.position);
diff.normalize();
diff.divideScalar(d);
steer.add(diff);
count++;
}
}
if (count > 0) steer.divideScalar(count);
if (steer.length() > 0) {
steer.setLength(this.maxSpeed);
steer.sub(this.velocity);
steer.clampLength(0, this.maxForce);
}
return steer;
}
align(boids) {
const neighborDist = 50;
const sum = new THREE.Vector3();
let count = 0;
for (const other of boids) {
const d = this.mesh.position.distanceTo(other.mesh.position);
if ((d > 0) && (d < neighborDist)) {
sum.add(other.velocity);
count++;
}
}
if (count > 0) {
sum.divideScalar(count);
sum.setLength(this.maxSpeed);
const steer = sum.sub(this.velocity);
steer.clampLength(0, this.maxForce);
return steer;
}
return new THREE.Vector3();
}
cohere(boids) {
const neighborDist = 50;
const sum = new THREE.Vector3();
let count = 0;
for (const other of boids) {
const d = this.mesh.position.distanceTo(other.mesh.position);
if ((d > 0) && (d < neighborDist)) {
sum.add(other.mesh.position);
count++;
}
}
if (count > 0) {
sum.divideScalar(count);
const desired = sum.sub(this.mesh.position);
desired.setLength(this.maxSpeed);
const steer = desired.sub(this.velocity);
steer.clampLength(0, this.maxForce);
return steer;
}
return new THREE.Vector3();
}
}
class School {
constructor(scene, count, modelFn, scale) {
this.boids = [];
this.target = new THREE.Vector3((Math.random() - 0.5) * worldSize, waterLevel - 50, (Math.random() - 0.5) * worldSize);
const model = modelFn();
model.scale.set(scale, scale, scale);
for(let i=0; i<count; i++){
const boidMesh = model.clone();
boidMesh.position.set(
(Math.random() - 0.5) * 200,
waterLevel - 50 + (Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 200
);
scene.add(boidMesh);
this.boids.push(new Boid(boidMesh));
}
}
update() {
if(this.boids[0].mesh.position.distanceTo(this.target) < 200){
this.target.set((Math.random() - 0.5) * worldSize, waterLevel - 50 - Math.random() * 50, (Math.random() - 0.5) * worldSize);
}
for (const boid of this.boids) {
boid.update(this.boids, this);
}
}
}
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a1429, 0.0035);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, worldSize * 1.5);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0x6688aa, 2));
const sun = new THREE.DirectionalLight(0xffffff, 2.5);
sun.position.set(100, 200, 100);
scene.add(sun);
const godrayLight = new THREE.DirectionalLight(0x8eadd4, 1.5);
godrayLight.position.set(200, 300, 200);
scene.add(godrayLight);
// ... water and sky setup (no changes)
const waterGeometry = new THREE.PlaneGeometry(worldSize * 2, worldSize * 2);
water = new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: new THREE.TextureLoader().load('https://cdn.jsdelivr.net/npm/three@0.165.0/examples/textures/waternormals.jpg', (t) => { t.wrapS = t.wrapT = THREE.RepeatWrapping; }), sunDirection: sun.position.clone().normalize(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: scene.fog !== undefined });
water.rotation.x = -Math.PI / 2;
water.position.y = waterLevel;
scene.add(water);
sky = new Sky();
sky.scale.setScalar(worldSize);
scene.add(sky);
const skyUniforms = sky.material.uniforms;
skyUniforms['turbidity'].value = 10; skyUniforms['rayleigh'].value = 2; skyUniforms['mieCoefficient'].value = 0.005; skyUniforms['mieDirectionalG'].value = 0.8;
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const phi = THREE.MathUtils.degToRad(88); const theta = THREE.MathUtils.degToRad(170);
sun.position.setFromSphericalCoords(1, phi, theta);
sky.material.uniforms['sunPosition'].value.copy(sun.position);
scene.environment = pmremGenerator.fromScene(sky).texture;
createTerrain();
createMermaid();
camera.position.set(mermaid.position.x, mermaid.position.y, mermaid.position.z + 15);
createFlora(200);
createMermaidCity(-worldSize/2 + 500, -worldSize/2 + 500);
createSurfaceWildlife(50);
createLilyPads(100);
// Create fish schools
schools.push(new School(scene, 50, createPikeModel, 1.0));
schools.push(new School(scene, 80, createSunfishModel, 1.5));
schools.push(new School(scene, 60, createBullheadModel, 1.2));
setupUI();
window.addEventListener('resize', onWindowResize, false);
document.addEventListener('keydown', (e) => controls[e.code] = true);
document.addEventListener('keyup', (e) => controls[e.code] = false);
renderer.render(scene, camera);
}
function setupUI() { /* ... no changes ... */ }
function createTerrain() { /* ... no changes ... */ }
function createMermaid() { /* ... no changes ... */ }
function onWindowResize() { /* ... no changes ... */ }
// Copying unchanged functions for brevity
setupUI = () => {const s=document.getElementById('start-button'),b=document.getElementById('blocker'),l=document.getElementById('lyric-ticker-text'),c=document.getElementById('speed-slider');l.textContent=rhymingWords;function u(){const n=20,t=200,i=t-((c.value-1)/99)*(t-n);l.style.animationDuration=`${i}s`}c.addEventListener('input',u);u();s.addEventListener('click',()=>{b.style.display='none';document.getElementById('song').play().catch(e=>console.error("Audio play failed:",e));animate()})};
createTerrain = () => {const s=worldSize,e=100,g=new THREE.PlaneGeometry(s,s,e,e);g.rotateX(-Math.PI/2);const n=new SimplexNoise(),p=g.attributes.position;for(let i=0;i<p.count;i++){const x=p.getX(i),z=p.getZ(i);let y=10*n.noise(x/500,z/500)-30*n.noise(x/800,z/800)-15*(Math.abs(n.noise(x/200,z/200))**2);p.setY(i,Math.max(y,-50))}g.computeVertexNormals();const m=new THREE.MeshStandardMaterial({color:0x3c322a,roughness:0.8,metalness:0.1});terrain=new THREE.Mesh(g,m);scene.add(terrain)};
createMermaid = () => {mermaid=new THREE.Group();const s=new THREE.MeshStandardMaterial({color:0x89CFF0,metalness:0.5,roughness:0.2}),t=new THREE.MeshStandardMaterial({color:0x008080,metalness:0.6,roughness:0.1,emissive:0x002222}),h=new THREE.MeshStandardMaterial({color:0xff4500,roughness:0.8});const o=new THREE.Mesh(new THREE.CapsuleGeometry(0.5,1.5,4,8),s);o.position.y=1.0;mermaid.add(o);const a=new THREE.Mesh(new THREE.SphereGeometry(0.7,16,12),s);a.position.y=2.5;mermaid.add(a);const e=new THREE.MeshBasicMaterial({color:0xffffff}),p=new THREE.MeshBasicMaterial({color:0x000000});mermaidState.leftEye=new THREE.Mesh(new THREE.SphereGeometry(0.15,8,8),e);mermaidState.rightEye=new THREE.Mesh(new THREE.SphereGeometry(0.15,8,8),e);const l=new THREE.Mesh(new THREE.SphereGeometry(0.08,8,8),p),r=new THREE.Mesh(new THREE.SphereGeometry(0.08,8,8),p);mermaidState.leftEye.add(l);mermaidState.rightEye.add(r);l.position.z=0.1;r.position.z=0.1;mermaidState.leftEye.position.set(-0.25,2.6,0.55);mermaidState.rightEye.position.set(0.25,2.6,0.55);mermaid.add(mermaidState.leftEye,mermaidState.rightEye);let c=mermaid;for(let i=0;i<8;i++){const g=new THREE.Mesh(new THREE.SphereGeometry(0.4-i*0.04,8,6),t);g.position.y=-0.4;c.add(g);mermaidState.tailSegments.push(g);c=g}const d=new THREE.ShapeGeometry(new THREE.Shape([new THREE.Vector2(0,0),new THREE.Vector2(1.5,0.5),new THREE.Vector2(1,1.5),new THREE.Vector2(0,1),new THREE.Vector2(-1,1.5),new THREE.Vector2(-1.5,0.5)]));const m=new THREE.Mesh(d,t);m.rotation.x=Math.PI/2;m.position.y=-0.5;c.add(m);for(let i=0;i<5;i++){const u=new THREE.CatmullRomCurve3([new THREE.Vector3(0,0,0),new THREE.Vector3(Math.random()-0.5,-1,Math.random()-0.5),new THREE.Vector3((Math.random()-0.5)*2,-3,(Math.random()-0.5)*2),new THREE.Vector3((Math.random()-0.5)*3,-5,(Math.random()-0.5)*3)]);const f=new THREE.TubeGeometry(u,20,0.05,5,false),w=new THREE.Mesh(f,h);w.position.y=2.8;mermaidState.hairStrands.push(w);mermaid.add(w)}mermaid.position.set(0,waterLevel-10,0);scene.add(mermaid)};
onWindowResize = () => {camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight)};
// --- NEW/UPDATED WORLD CREATION ---
function createFlora(count) {
const plantRule = { 'F': 'FF+[+F-F-F]-[-F+F+F]' };
const plantAxiom = 'F';
const plantLSystem = generateLSystem(plantAxiom, plantRule, 3);
const plantGeom = createLSystemGeometry(plantLSystem, THREE.MathUtils.degToRad(25), 0.5);
plantGeom.scale(2,2,2);
const material = new THREE.MeshStandardMaterial({color: 0x228B22, emissive: 0x113311, side: THREE.DoubleSide});
for(let i=0; i < count; i++) {
const plantMesh = new THREE.Mesh(plantGeom, material);
const x = (Math.random() - 0.5) * worldSize;
const z = (Math.random() - 0.5) * worldSize;
raycaster.set(new THREE.Vector3(x, waterLevel, z), downVector);
const intersects = raycaster.intersectObject(terrain);
if(intersects.length > 0) {
plantMesh.position.copy(intersects[0].point);
plantMesh.rotation.set(0, Math.random() * Math.PI * 2, 0);
scene.add(plantMesh);
}
}
}
function createMermaidCity(x_offset, z_offset) {
const cityRule = {'F': 'F[+FF][-FF]F[-F][+F]F'};
const cityAxiom = 'F';
const cityLSystem = generateLSystem(cityAxiom, cityRule, 4);
const cityGeom = createLSystemGeometry(cityLSystem, THREE.MathUtils.degToRad(20), 5);
const material = new THREE.MeshStandardMaterial({ color: 0x77aaff, emissive: 0x88ccff, emissiveIntensity: 0.8, roughness: 0.6 });
for(let i = 0; i < 20; i++){
const building = new THREE.Mesh(cityGeom, material);
building.position.set(
x_offset + (Math.random() - 0.5) * 800,
-50,
z_offset + (Math.random() - 0.5) * 800
);
building.scale.setScalar(1 + Math.random() * 2);
building.rotation.set(0, Math.random() * Math.PI * 2, 0);
scene.add(building);
}
}
function createPikeModel() {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x90ee90, roughness: 0.5, metalness: 0.2});
const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.3, 2.5, 4, 8), bodyMat);
body.rotation.z = Math.PI / 2;
const tail = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.8, 0.8), bodyMat);
tail.position.x = -1.3;
group.add(body, tail);
return group;
}
function createSunfishModel() {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x6495ed, roughness: 0.5, metalness: 0.2});
const body = new THREE.Mesh(new THREE.SphereGeometry(1, 8, 6), bodyMat);
body.scale.set(1.5, 1, 0.3);
const tail = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 0.1), bodyMat);
tail.position.x = -1;
group.add(body, tail);
return group;
}
function createBullheadModel() {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.8});
const body = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.8, 1), bodyMat);
const tail = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1, 4), bodyMat);
tail.rotation.z = -Math.PI / 2;
tail.position.x = -1;
group.add(body, tail);
return group;
}
function createSurfaceWildlife(count) {
const mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(1, 0.5, 2), new THREE.MeshLambertMaterial({color: 0xeeeeee}), count);
const dummy = new THREE.Object3D();
for(let i=0; i<count; i++){
dummy.position.set((Math.random() - 0.5) * worldSize, waterLevel, (Math.random() - 0.5) * worldSize);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
scene.add(mesh);
}
function createLilyPads(count) {
const g = new THREE.CircleGeometry(1, 8);
g.rotateX(-Math.PI/2);
const mesh = new THREE.InstancedMesh(g, new THREE.MeshLambertMaterial({color: 0x006400}), count);
const dummy = new THREE.Object3D();
for(let i=0; i<count; i++){
dummy.position.set((Math.random() - 0.5) * worldSize, waterLevel + 0.1, (Math.random() - 0.5) * worldSize);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
scene.add(mesh);
}
function updateMermaid() { /* ... no changes ... */ }
updateMermaid = () => {const d=clock.getDelta(),t=clock.getElapsedTime();const m=30.0,r=1.0;let a=0,v=0,f=0;if(controls['KeyW'])f=1.0;if(controls['KeyS'])f=-0.5;if(controls['KeyA'])a=r;if(controls['KeyD'])a=-r;if(controls['Space'])v=10.0;if(controls['ShiftLeft']||controls['ShiftRight'])v=-10.0;mermaidState.turnSpeed=THREE.MathUtils.lerp(mermaidState.turnSpeed,a,d*2.0);mermaid.rotation.y+=mermaidState.turnSpeed*d;mermaidState.forwardSpeed=THREE.MathUtils.lerp(mermaidState.forwardSpeed,m*f,d*1.5);const w=new THREE.Vector3(0,0,-1).applyQuaternion(mermaid.quaternion);mermaidVelocity.x=w.x*mermaidState.forwardSpeed;mermaidVelocity.z=w.z*mermaidState.forwardSpeed;const b=0.5;mermaidVelocity.y=THREE.MathUtils.lerp(mermaidVelocity.y,v||b,d*2.0);mermaid.position.addScaledVector(mermaidVelocity,d);raycaster.set(mermaid.position,downVector);const i=raycaster.intersectObject(terrain);if(i.length>0){const g=i[0].point.y;if(mermaid.position.y<g+3.0){mermaid.position.y=g+3.0;mermaidVelocity.y=Math.max(mermaidVelocity.y,2.0)}}mermaid.position.y=Math.min(mermaid.position.y,waterLevel-1);mermaid.rotation.z=THREE.MathUtils.lerp(mermaid.rotation.z,mermaidState.turnSpeed*-0.5,d*2.0);const p=-mermaidVelocity.y*0.05;mermaid.rotation.x=THREE.MathUtils.lerp(mermaid.rotation.x,p,d*2.5);const s=Math.abs(mermaidState.forwardSpeed/m);mermaidState.tailSegments.forEach((e,i)=>{const n=Math.sin(t*6.0-i*0.6)*(0.1+s*0.6);e.rotation.y=n;e.rotation.z=n*0.5});mermaidState.hairStrands.forEach((e,i)=>{e.rotation.x=Math.sin(t*1.5+i)*0.1-s*0.1;e.rotation.z=Math.sin(t*1.5+i)*0.1});const j=Math.sin(t*20)*0.02;mermaidState.leftEye.children[0].position.x=j;mermaidState.rightEye.children[0].position.x=-j;const o=new THREE.Vector3(0,4,18);o.applyQuaternion(mermaid.quaternion);camera.position.lerp(mermaid.position.clone().add(o),d*2.0);camera.lookAt(mermaid.position.clone().add(new THREE.Vector3(0,2,0)))};
function animate() {
animationId = requestAnimationFrame(animate);
updateMermaid();
for (const school of schools) {
school.update();
}
water.material.uniforms['time'].value += 1.0 / 60.0;
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>