Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Lake Minnetonka - Mound Docks at Dusk</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
background: #1a1a2e; | |
overflow: hidden; | |
font-family: 'Georgia', serif; | |
} | |
canvas { | |
display: block; | |
} | |
.controls { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
color: #d4af37; | |
background: rgba(0, 0, 0, 0.7); | |
padding: 15px; | |
border-radius: 10px; | |
border: 1px solid #8B4513; | |
z-index: 100; | |
} | |
.controls h3 { | |
margin: 0 0 10px 0; | |
color: #daa520; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.8); | |
} | |
.controls p { | |
margin: 5px 0; | |
font-size: 14px; | |
text-shadow: 1px 1px 2px rgba(0,0,0,0.8); | |
} | |
.title { | |
position: absolute; | |
bottom: 30px; | |
left: 50%; | |
transform: translateX(-50%); | |
color: #d4af37; | |
text-align: center; | |
z-index: 100; | |
background: rgba(0, 0, 0, 0.8); | |
padding: 15px 25px; | |
border-radius: 15px; | |
border: 2px solid #8B4513; | |
} | |
.title h1 { | |
margin: 0; | |
font-size: 24px; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.8); | |
color: #daa520; | |
} | |
.title p { | |
margin: 5px 0 0 0; | |
font-style: italic; | |
font-size: 16px; | |
text-shadow: 1px 1px 2px rgba(0,0,0,0.8); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="controls"> | |
<h3>Lake Minnetonka Experience</h3> | |
<p>🎮 Mouse: Look around</p> | |
<p>⬅️➡️ Arrow keys: Move left/right</p> | |
<p>⬆️⬇️ Arrow keys: Move forward/back</p> | |
<p>🔧 WASD: Alternative movement</p> | |
<p>✨ Watch the fireflies rise at dusk</p> | |
</div> | |
<div class="title"> | |
<h1>Mound Docks at Dusk</h1> | |
<p>"These Mound docks at dusk are almost more than I can bear..."</p> | |
</div> | |
<script> | |
// Scene setup | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
renderer.fog = true; | |
document.body.appendChild(renderer.domElement); | |
// Atmospheric fog | |
scene.fog = new THREE.Fog(0x2d1810, 50, 800); | |
// Camera controls | |
let mouseX = 0, mouseY = 0; | |
let cameraRotationX = 0, cameraRotationY = 0; | |
const keys = {}; | |
// Position camera on the dock | |
camera.position.set(0, 3, 15); | |
camera.lookAt(0, 0, 0); | |
// Create bruising dusk sky | |
const skyGeometry = new THREE.SphereGeometry(1000, 32, 32); | |
const skyMaterial = new THREE.ShaderMaterial({ | |
uniforms: { | |
time: { value: 0 } | |
}, | |
vertexShader: ` | |
varying vec3 vWorldPosition; | |
void main() { | |
vec4 worldPosition = modelMatrix * vec4(position, 1.0); | |
vWorldPosition = worldPosition.xyz; | |
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
} | |
`, | |
fragmentShader: ` | |
uniform float time; | |
varying vec3 vWorldPosition; | |
void main() { | |
vec3 direction = normalize(vWorldPosition); | |
float elevation = direction.y; | |
// Bruising sky colors | |
vec3 darkPurple = vec3(0.1, 0.05, 0.2); | |
vec3 deepBlue = vec3(0.05, 0.1, 0.3); | |
vec3 rustGold = vec3(0.8, 0.4, 0.1); | |
vec3 bloodRed = vec3(0.6, 0.1, 0.1); | |
// Sun bleeding effect on horizon | |
float horizonGlow = exp(-abs(elevation) * 2.0); | |
float sunBleed = sin(direction.x * 2.0 + time * 0.5) * 0.3 + 0.7; | |
vec3 color = mix(darkPurple, deepBlue, elevation + 0.5); | |
color = mix(color, rustGold, horizonGlow * sunBleed * 0.8); | |
color = mix(color, bloodRed, horizonGlow * 0.3); | |
gl_FragColor = vec4(color, 1.0); | |
} | |
`, | |
side: THREE.BackSide | |
}); | |
const sky = new THREE.Mesh(skyGeometry, skyMaterial); | |
scene.add(sky); | |
// Create water surface with bleeding sun reflection | |
const waterGeometry = new THREE.PlaneGeometry(1000, 1000, 128, 128); | |
const waterMaterial = new THREE.ShaderMaterial({ | |
uniforms: { | |
time: { value: 0 }, | |
sunPosition: { value: new THREE.Vector3(100, 10, -200) } | |
}, | |
vertexShader: ` | |
uniform float time; | |
varying vec2 vUv; | |
varying vec3 vPosition; | |
void main() { | |
vUv = uv; | |
// Wave animation | |
vec3 pos = position; | |
float wave1 = sin(pos.x * 0.02 + time) * 0.3; | |
float wave2 = sin(pos.y * 0.015 + time * 1.3) * 0.2; | |
float wave3 = sin((pos.x + pos.y) * 0.01 + time * 0.8) * 0.4; | |
pos.z = wave1 + wave2 + wave3; | |
vPosition = pos; | |
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); | |
} | |
`, | |
fragmentShader: ` | |
uniform float time; | |
uniform vec3 sunPosition; | |
varying vec2 vUv; | |
varying vec3 vPosition; | |
void main() { | |
// Base water color | |
vec3 deepWater = vec3(0.1, 0.2, 0.3); | |
vec3 shallowWater = vec3(0.2, 0.3, 0.4); | |
// Sun bleeding reflection - rust and gold | |
vec3 rustColor = vec3(0.8, 0.3, 0.1); | |
vec3 goldColor = vec3(1.0, 0.8, 0.2); | |
// Calculate distance from sun reflection point | |
vec2 sunReflection = vec2(0.0, -0.3); // Approximate sun position on water | |
float distToSun = length(vUv - sunReflection); | |
// Bleeding sun effect | |
float sunIntensity = exp(-distToSun * 3.0) * (sin(time) * 0.2 + 0.8); | |
vec3 sunBleed = mix(rustColor, goldColor, sin(time * 2.0) * 0.5 + 0.5); | |
// Wave reflections | |
float wave = sin(vPosition.x * 0.1 + time) * sin(vPosition.y * 0.08 + time * 1.2); | |
vec3 waterColor = mix(deepWater, shallowWater, wave * 0.5 + 0.5); | |
// Final color with bleeding sun | |
vec3 finalColor = mix(waterColor, sunBleed, sunIntensity); | |
gl_FragColor = vec4(finalColor, 0.9); | |
} | |
`, | |
transparent: true | |
}); | |
waterGeometry.rotateX(-Math.PI / 2); | |
const water = new THREE.Mesh(waterGeometry, waterMaterial); | |
water.position.y = 0; | |
scene.add(water); | |
// Create wooden pier with weathered texture | |
function createPier() { | |
const pierGroup = new THREE.Group(); | |
// Main pier planks | |
for (let i = 0; i < 20; i++) { | |
const plankGeometry = new THREE.BoxGeometry(2, 0.1, 0.8); | |
const plankMaterial = new THREE.MeshLambertMaterial({ | |
color: new THREE.Color(0.4 + Math.random() * 0.2, 0.25, 0.15) | |
}); | |
const plank = new THREE.Mesh(plankGeometry, plankMaterial); | |
plank.position.set(0, 0.5, -i * 0.9); | |
plank.castShadow = true; | |
pierGroup.add(plank); | |
} | |
// Pier supports | |
for (let i = 0; i < 5; i++) { | |
const supportGeometry = new THREE.CylinderGeometry(0.1, 0.1, 3); | |
const supportMaterial = new THREE.MeshLambertMaterial({ color: 0x3d2817 }); | |
const support = new THREE.Mesh(supportGeometry, supportMaterial); | |
support.position.set(-0.8, -0.5, -i * 4); | |
pierGroup.add(support); | |
const support2 = support.clone(); | |
support2.position.x = 0.8; | |
pierGroup.add(support2); | |
} | |
return pierGroup; | |
} | |
const pier = createPier(); | |
scene.add(pier); | |
// Create pontoon boat "hanging its head" | |
function createPontoon() { | |
const pontoonGroup = new THREE.Group(); | |
// Pontoon floats | |
const pontoonGeometry = new THREE.CylinderGeometry(0.3, 0.3, 8); | |
const pontoonMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 }); | |
const leftPontoon = new THREE.Mesh(pontoonGeometry, pontoonMaterial); | |
leftPontoon.rotation.z = Math.PI / 2; | |
leftPontoon.position.set(-1.5, 0.3, -25); | |
pontoonGroup.add(leftPontoon); | |
const rightPontoon = leftPontoon.clone(); | |
rightPontoon.position.x = 1.5; | |
pontoonGroup.add(rightPontoon); | |
// Deck | |
const deckGeometry = new THREE.BoxGeometry(4, 0.1, 8); | |
const deckMaterial = new THREE.MeshLambertMaterial({ color: 0x8B7355 }); | |
const deck = new THREE.Mesh(deckGeometry, deckMaterial); | |
deck.position.set(0, 0.8, -25); | |
pontoonGroup.add(deck); | |
// Mast for morse code slapping | |
const mastGeometry = new THREE.CylinderGeometry(0.05, 0.05, 6); | |
const mastMaterial = new THREE.MeshLambertMaterial({ color: 0x654321 }); | |
const mast = new THREE.Mesh(mastGeometry, mastMaterial); | |
mast.position.set(0, 3.8, -25); | |
pontoonGroup.add(mast); | |
// Make pontoon "hang its head" - slight rotation | |
pontoonGroup.rotation.x = 0.1; | |
pontoonGroup.position.y = -0.3; // Lower than it was "back then" | |
return pontoonGroup; | |
} | |
const pontoon = createPontoon(); | |
scene.add(pontoon); | |
// Create plastic chair with sweating beer | |
function createChairAndBeer() { | |
const chairGroup = new THREE.Group(); | |
// Plastic chair | |
const seatGeometry = new THREE.BoxGeometry(1.5, 0.1, 1.5); | |
const chairMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); | |
const seat = new THREE.Mesh(seatGeometry, chairMaterial); | |
seat.position.set(3, 1, 8); | |
chairGroup.add(seat); | |
// Chair back | |
const backGeometry = new THREE.BoxGeometry(1.5, 2, 0.1); | |
const back = new THREE.Mesh(backGeometry, chairMaterial); | |
back.position.set(3, 2, 7.3); | |
chairGroup.add(back); | |
// Chair legs | |
for (let i = 0; i < 4; i++) { | |
const legGeometry = new THREE.CylinderGeometry(0.03, 0.03, 1); | |
const leg = new THREE.Mesh(legGeometry, chairMaterial); | |
leg.position.set( | |
3 + (i % 2 ? 0.6 : -0.6), | |
0.5, | |
8 + (i < 2 ? 0.6 : -0.6) | |
); | |
chairGroup.add(leg); | |
} | |
// Beer can | |
const beerGeometry = new THREE.CylinderGeometry(0.15, 0.15, 0.5); | |
const beerMaterial = new THREE.MeshLambertMaterial({ color: 0xDAA520 }); | |
const beer = new THREE.Mesh(beerGeometry, beerMaterial); | |
beer.position.set(3.8, 1.35, 8); | |
chairGroup.add(beer); | |
return chairGroup; | |
} | |
const chairAndBeer = createChairAndBeer(); | |
scene.add(chairAndBeer); | |
// Create scattered tackle and rusty magnet | |
function createTackle() { | |
const tackleGroup = new THREE.Group(); | |
// Scattered tackle items | |
for (let i = 0; i < 15; i++) { | |
const tackleGeometry = new THREE.SphereGeometry(0.1, 8, 8); | |
const tackleMaterial = new THREE.MeshLambertMaterial({ | |
color: new THREE.Color(Math.random(), Math.random(), Math.random()) | |
}); | |
const tackle = new THREE.Mesh(tackleGeometry, tackleMaterial); | |
tackle.position.set( | |
(Math.random() - 0.5) * 10, | |
0.6, | |
Math.random() * 5 | |
); | |
tackleGroup.add(tackle); | |
} | |
// Rusty magnet on fraying twine | |
const magnetGeometry = new THREE.BoxGeometry(0.3, 0.1, 0.3); | |
const magnetMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); | |
const magnet = new THREE.Mesh(magnetGeometry, magnetMaterial); | |
magnet.position.set(-2, 0.6, 5); | |
tackleGroup.add(magnet); | |
return tackleGroup; | |
} | |
const tackle = createTackle(); | |
scene.add(tackle); | |
// Create mansions on the shore | |
function createMansions() { | |
const mansionGroup = new THREE.Group(); | |
for (let i = 0; i < 5; i++) { | |
const mansionGeometry = new THREE.BoxGeometry( | |
8 + Math.random() * 4, | |
6 + Math.random() * 4, | |
12 + Math.random() * 6 | |
); | |
const mansionMaterial = new THREE.MeshLambertMaterial({ | |
color: new THREE.Color(0.9, 0.9, 0.8) | |
}); | |
const mansion = new THREE.Mesh(mansionGeometry, mansionMaterial); | |
mansion.position.set( | |
40 + i * 25 + Math.random() * 10, | |
mansion.geometry.parameters.height / 2, | |
-100 + Math.random() * 200 | |
); | |
mansion.castShadow = true; | |
mansionGroup.add(mansion); | |
} | |
return mansionGroup; | |
} | |
const mansions = createMansions(); | |
scene.add(mansions); | |
// Fireflies rising like embers | |
const fireflies = []; | |
const fireflyGeometry = new THREE.SphereGeometry(0.1, 8, 8); | |
const fireflyMaterial = new THREE.MeshBasicMaterial({ | |
color: 0xFFFF80, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
for (let i = 0; i < 100; i++) { | |
const firefly = new THREE.Mesh(fireflyGeometry, fireflyMaterial); | |
firefly.position.set( | |
(Math.random() - 0.5) * 100, | |
Math.random() * 2, | |
(Math.random() - 0.5) * 100 | |
); | |
firefly.userData = { | |
speed: Math.random() * 0.02 + 0.01, | |
phase: Math.random() * Math.PI * 2 | |
}; | |
fireflies.push(firefly); | |
scene.add(firefly); | |
} | |
// Lighting setup | |
const ambientLight = new THREE.AmbientLight(0x404060, 0.3); | |
scene.add(ambientLight); | |
// Sunset directional light (bleeding sun) | |
const sunLight = new THREE.DirectionalLight(0xFF6B35, 0.8); | |
sunLight.position.set(100, 20, -200); | |
sunLight.castShadow = true; | |
sunLight.shadow.mapSize.width = 2048; | |
sunLight.shadow.mapSize.height = 2048; | |
scene.add(sunLight); | |
// Warm dusk light | |
const duskLight = new THREE.DirectionalLight(0xDAA520, 0.4); | |
duskLight.position.set(-50, 30, 100); | |
scene.add(duskLight); | |
// Point light for fireflies area | |
const fireflyLight = new THREE.PointLight(0xFFFF80, 0.5, 30); | |
fireflyLight.position.set(0, 5, 0); | |
scene.add(fireflyLight); | |
// Controls | |
document.addEventListener('mousemove', (event) => { | |
mouseX = (event.clientX / window.innerWidth) * 2 - 1; | |
mouseY = (event.clientY / window.innerHeight) * 2 - 1; | |
}); | |
document.addEventListener('keydown', (event) => { | |
keys[event.code] = true; | |
}); | |
document.addEventListener('keyup', (event) => { | |
keys[event.code] = false; | |
}); | |
// Animation loop | |
let time = 0; | |
function animate() { | |
requestAnimationFrame(animate); | |
time += 0.01; | |
// Update shaders | |
skyMaterial.uniforms.time.value = time; | |
waterMaterial.uniforms.time.value = time; | |
// Camera controls | |
cameraRotationY += (mouseX * 0.5 - cameraRotationY) * 0.05; | |
cameraRotationX += (mouseY * 0.3 - cameraRotationX) * 0.05; | |
cameraRotationX = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, cameraRotationX)); | |
// Movement | |
const moveSpeed = 0.3; | |
if (keys['ArrowUp'] || keys['KeyW']) { | |
camera.position.z -= Math.cos(cameraRotationY) * moveSpeed; | |
camera.position.x -= Math.sin(cameraRotationY) * moveSpeed; | |
} | |
if (keys['ArrowDown'] || keys['KeyS']) { | |
camera.position.z += Math.cos(cameraRotationY) * moveSpeed; | |
camera.position.x += Math.sin(cameraRotationY) * moveSpeed; | |
} | |
if (keys['ArrowLeft'] || keys['KeyA']) { | |
camera.position.x -= Math.cos(cameraRotationY) * moveSpeed; | |
camera.position.z += Math.sin(cameraRotationY) * moveSpeed; | |
} | |
if (keys['ArrowRight'] || keys['KeyD']) { | |
camera.position.x += Math.cos(cameraRotationY) * moveSpeed; | |
camera.position.z -= Math.sin(cameraRotationY) * moveSpeed; | |
} | |
// Apply camera rotation | |
camera.rotation.x = cameraRotationX; | |
camera.rotation.y = cameraRotationY; | |
// Animate fireflies rising like embers | |
fireflies.forEach((firefly, index) => { | |
firefly.position.y += firefly.userData.speed; | |
firefly.position.x += Math.sin(time + firefly.userData.phase) * 0.01; | |
firefly.position.z += Math.cos(time + firefly.userData.phase) * 0.01; | |
// Reset fireflies that get too high | |
if (firefly.position.y > 20) { | |
firefly.position.y = 0; | |
firefly.position.x = (Math.random() - 0.5) * 100; | |
firefly.position.z = (Math.random() - 0.5) * 100; | |
} | |
// Flickering effect | |
firefly.material.opacity = 0.3 + Math.sin(time * 5 + index) * 0.5; | |
}); | |
// Gentle pier swaying | |
pier.rotation.z = Math.sin(time * 0.5) * 0.02; | |
// Pontoon bobbing (breathing slower) | |
pontoon.position.y = -0.3 + Math.sin(time * 0.3) * 0.1; | |
pontoon.rotation.x = 0.1 + Math.sin(time * 0.4) * 0.05; | |
// Mast slapping animation (morse code) | |
const mast = pontoon.children.find(child => child.geometry?.parameters?.height === 6); | |
if (mast) { | |
mast.rotation.z = Math.sin(time * 2) * 0.1; | |
} | |
renderer.render(scene, camera); | |
} | |
// Handle window resize | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
// Start animation | |
animate(); | |
</script> | |
</body> | |
</html> |