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