```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Julimes | The Rugged Frontier</title> <!-- Tailwind CSS --> <script src="https://cdn.tailwindcss.com"></script> <!-- Google Fonts --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,400&family=Work+Sans:wght@300;400;500&display=swap" rel="stylesheet"> <!-- Three.js --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <!-- GSAP for smooth scroll animation --> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script> <!-- Simplex Noise for terrain generation --> <script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script> <script> tailwind.config = { theme: { extend: { colors: { desert: { 100: '#F4ECD8', // Sand 200: '#E6D5B8', // Tan 300: '#D4B896', // Khaki 400: '#C19A6B', // Bronze dark: '#5D4037', // Dark Soil accent: '#20B2AA', // Rio Conchos (Seafoam/Murky) }, sage: '#8FBC8F', terracotta: '#CD7532' }, fontFamily: { serif: ['"Cormorant Garamond"', 'serif'], sans: ['"Work Sans"', 'sans-serif'], }, spacing: { '128': '32rem', } } } } </script> <style> body, html { margin: 0; padding: 0; overflow-x: hidden; background-color: #F4ECD8; color: #3E2723; } /* Paper Grain Overlay */ .grain-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50; opacity: 0.08; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); } /* Smooth Scrolling */ html { scroll-behavior: smooth; } /* Text Content Container Styling */ .glass-panel { background: rgba(244, 236, 216, 0.75); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(210, 180, 140, 0.3); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.05); } .organic-blob { border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px; } /* Loader */ #loader { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #F4ECD8; z-index: 100; display: flex; justify-content: center; align-items: center; font-family: 'Cormorant Garamond', serif; font-size: 1.5rem; color: #5D4037; transition: opacity 1s ease-out; } #canvas-container { position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 0; } .content-section { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 2rem; position: relative; z-index: 20; pointer-events: none; /* Let clicks pass through empty areas */ } .content-container { max-width: 45rem; pointer-events: auto; } /* Hide scrollbar for cleaner look but allow scrolling */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #F4ECD8; } ::-webkit-scrollbar-thumb { background: #C19A6B; border-radius: 4px; } </style> </head> <body> <!-- Grain Texture --> <div class="grain-overlay"></div> <!-- Loading Screen --> <div id="loader"> <div class="flex flex-col items-center"> <span class="mb-4 italic">Entering the Frontier</span> <div class="w-16 h-1 bg-terracotta opacity-50 rounded-full overflow-hidden"> <div class="h-full bg-terracotta animate-pulse w-2/3"></div> </div> </div> </div> <!-- 3D Canvas --> <div id="canvas-container"></div> <!-- Text Content --> <main> <!-- Hero Section --> <section class="content-section" id="hero"> <div class="content-container text-center"> <div class="glass-panel organic-blob p-8 md:p-12 transform transition hover:scale-105 duration-[2s] ease-out"> <h1 class="font-serif text-6xl md:text-8xl text-desert-dark mb-4 italic">Julimes</h1> <p class="font-sans text-lg md:text-xl text-stone-700 tracking-wide uppercase border-t border-stone-400 pt-4 mt-4"> Chihuahua • Mexico </p> </div> </div> </section> <!-- Geography Section --> <section class="content-section" id="geography"> <div class="content-container"> <div class="glass-panel organic-blob p-8 md:p-10 md:rounded-3xl"> <h2 class="font-serif text-4xl md:text-5xl text-desert-dark mb-6">Vast Northern Expanse</h2> <p class="font-serif text-xl md:text-2xl text-stone-700 leading-relaxed mb-6"> Nestled within the vast northern expanse of the Mexican state of Chihuahua, Julimes represents a quintessential, rugged frontier territory. </p> <p class="font-sans text-lg leading-loose text-stone-800"> As a second-level administrative division, its identity is deeply shaped by the dramatic landscapes of the Lower Grande region, characterized by expansive deserts, rugged canyons, and the life-giving ribbon of the Rio Conchos. </p> </div> </div> </section> <!-- Economy Section --> <section class="content-section" id="economy"> <div class="content-container"> <div class="glass-panel organic-blob p-8 md:p-10 md:rounded-3xl"> <h2 class="font-serif text-4xl md:text-5xl text-desert-dark mb-6">The Rhythm of Land</h2> <p class="font-serif text-xl md:text-2xl text-stone-700 leading-relaxed mb-6"> This sparsely populated area sustains a traditional economy largely based on extensive cattle ranching and dryland agriculture, practices intrinsically adapted to the arid climate. </p> <div class="flex items-center gap-4 mt-8 text-stone-600"> <svg class="w-8 h-8 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> <span class="font-sans italic">Ranching • Resilience • Adaptation</span> </div> </div> </div> </section> <!-- Culture Section --> <section class="content-section" id="culture"> <div class="content-container"> <div class="glass-panel organic-blob p-8 md:p-10 md:rounded-3xl"> <h2 class="font-serif text-4xl md:text-5xl text-desert-dark mb-6">Heritage & Spirit</h2> <p class="font-serif text-xl md:text-2xl text-stone-700 leading-relaxed mb-6"> Culturally, it forms part of the broader northern Mexican <em>charrería</em> and ranching traditions, while also incorporating elements of the region's indigenous heritage. </p> <div class="grid md:grid-cols-2 gap-6 mt-8 font-sans text-stone-700"> <div class="p-4 border-l-4 border-terracotta"> <strong class="block text-desert-dark mb-2">Tradition</strong> The art of horsemanship and the charro suit are living symbols of this frontier identity. </div> <div class="p-4 border-l-4 border-sage"> <strong class="block text-desert-dark mb-2">Ancestry</strong> Indigenous roots whisper through the canyons, blending with Spanish colonial history. </div> </div> </div> </div> </section> <!-- Conclusion --> <section class="content-section" id="finale"> <div class="content-container text-center"> <div class="glass-panel organic-blob p-8 md:p-12"> <h2 class="font-serif text-3xl md:text-4xl text-desert-dark mb-6">Enduring Spirit</h2> <p class="font-serif text-2xl text-stone-700 italic mb-8"> "Julimes embodies the serene, isolated, and self-reliant character of rural Chihuahua, offering a stark contrast to the urban and industrial centers of the state, and stands as a testament to the enduring spirit of Mexico's northern frontier." </p> </div> </div> </section> <footer class="h-24 relative z-20 flex justify-center items-center text-stone-500 font-sans text-sm"> <p>© 2023 Julimes Tribute. Crafted with Biophilic Principles.</p> </footer> </main> <script> // --- Config --- const CONFIG = { fogColor: 0xe8d7ca, // Warm sandy fog skyColor: 0xfff0e6, // Pale morning sky sunColor: 0xffaa55, // Warm golden sun terrainColorLow: 0x8d6e63, // Brown terrainColorHigh: 0xd7ccc8, // Light sand rock waterColor: 0x2dd4bf, // Rio Conchos teal/murky green segmentCount: 64, // Grid res worldSize: 200, noiseScale: 0.03, riverWidth: 8, riverDepth: 12 }; // --- Init Scene --- const container = document.getElementById('canvas-container'); const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(CONFIG.fogColor, 0.008); scene.background = new THREE.Color(CONFIG.skyColor); const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 40, 100); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; container.appendChild(renderer.domElement); // --- Helpers --- const simplex = new SimplexNoise(); const getRiverDistance = (x, z) => { // Create a winding river path roughly diagonally from upper left to lower right // Using a combination of sine waves to simulate a natural meander // Adjusted to pass through (0,0) roughly const normalizedX = x / CONFIG.worldSize * 2; // approx -2 to 2 const pathY = Math.sin(normalizedX * 1.5) * (CONFIG.worldSize * 0.4) + Math.cos(normalizedX * 0.5) * (CONFIG.worldSize * 0.1); return Math.abs(z - pathY); }; // --- Terrain Mesh --- const geometry = new THREE.PlaneGeometry(CONFIG.worldSize, CONFIG.worldSize, CONFIG.segmentCount, CONFIG.segmentCount); // Rotate to be XZ plane geometry.rotateX(-Math.PI / 2); const positionAttribute = geometry.attributes.position; const vertex = new THREE.Vector3(); for (let i = 0; i < positionAttribute.count; i++) { vertex.fromBufferAttribute(positionAttribute, i); // 1. Base Noise let elevation = simplex.noise2D(vertex.x * CONFIG.noiseScale, vertex.z * CONFIG.noiseScale) * 10; elevation += simplex.noise2D(vertex.x * CONFIG.noiseScale * 2, vertex.z * CONFIG.noiseScale * 2) * 4; // 2. Carve River Valley const riverDist = getRiverDistance(vertex.x, vertex.z); const riverInfluence = Math.max(0, CONFIG.riverWidth - riverDist) / CONFIG.riverWidth; // 1 at center, 0 at edge // Smooth the influence curve const smoothInfluence = riverInfluence * riverInfluence * (3 - 2 * riverInfluence); elevation -= smoothInfluence * CONFIG.riverDepth; // Flatten very low areas to water level approx if(elevation < -5) elevation = -5; positionAttribute.setY(i, elevation); } geometry.computeVertexNormals(); // Vertex Colors for variation const colors = []; const colorLow = new THREE.Color(CONFIG.terrainColorLow); const colorHigh = new THREE.Color(CONFIG.terrainColorHigh); for (let i = 0; i < positionAttribute.count; i++) { const y = positionAttribute.getY(i); const normalizedHeight = (y + 8) / 18; // Normalize approx -8 to 10 range const clamped = Math.max(0, Math.min(1, normalizedHeight)); const color = colorLow.clone().lerp(colorHigh, clamped); colors.push(color.r, color.g, color.b); } geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); const material = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.9, metalness: 0.1, flatShading: true, // Low-poly look, Wabi-Sabi feel }); const terrain = new THREE.Mesh(geometry, material); terrain.receiveShadow = true; scene.add(terrain); // --- River Ribbon --- // Construct a ribbon along the river path const ribbonPoints = []; const steps = 100; const xStart = -CONFIG.worldSize/2 + 10; const xStep = CONFIG.worldSize / steps; for(let i=0; i<=steps; i++) { const x = xStart + i * xStep; const z = Math.sin(x * 0.015) * (CONFIG.worldSize * 0.4) + Math.cos(x * 0.005) * (CONFIG.worldSize * 0.15); ribbonPoints.push(new THREE.Vector3(x, -2, z)); // Slightly below average terrain } const curve = new THREE.CatmullRomCurve3(ribbonPoints); // Create tube geometry for river const ribbonGeo = new THREE.TubeGeometry(curve, 128, 2.5, 8, false); // Flatten it slightly to look like a river bed? Or just a ribbon flowing. // Let's make it a flat strip const ribbonMesh = new THREE.Mesh(ribbonGeo, new THREE.MeshLambertMaterial({ color: CONFIG.waterColor, transparent: true, opacity: 0.7, side: THREE.DoubleSide })); scene.add(ribbonMesh); // --- Lighting --- const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); scene.add(ambientLight); const sunLight = new THREE.DirectionalLight(CONFIG.sunColor, 1.2); sunLight.position.set(50, 80, 50); sunLight.castShadow = true; sunLight.shadow.mapSize.width = 2048; sunLight.shadow.mapSize.height = 2048; sunLight.shadow.camera.near = 0.5; sunLight.shadow.camera.far = 300; sunLight.shadow.camera.left = -100; sunLight.shadow.camera.right = 100; sunLight.shadow.camera.top = 100; sunLight.shadow.camera.bottom = -100; scene.add(sunLight); // --- Particles (Dust) --- const particlesGeo = new THREE.BufferGeometry(); const particleCount = 600; const posArray = new Float32Array(particleCount * 3); for(let i=0; i<particleCount * 3; i++) { posArray[i] = (Math.random() - 0.5) * CONFIG.worldSize * 1.5; } particlesGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); const particlesMat = new THREE.PointsMaterial({ size: 0.4, color: 0x5D4037, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending }); const particlesMesh = new THREE.Points(particlesGeo, particlesMat); scene.add(particlesMesh); // --- Interaction Logic --- let scrollPercent = 0; const totalHeight = document.body.scrollHeight - window.innerHeight; // Map scroll to camera flight path // Camera start: Hero view (0) // Camera end: Low angle flying over terrain const startPos = new THREE.Vector3(0, 30, 90); const endPos = new THREE.Vector3(20, 5, -60); // End near the bottom of page const startTarget = new THREE.Vector3(0, 0, 0); const endTarget = new THREE.Vector3(0, 0, 80); // Look up the river function updateCamera() { // Linear interpolation based on scroll const t = Math.max(0, Math.min(1, scrollPercent)); // Lerp position camera.position.lerpVectors(startPos, endPos, t); // Lerp target (lookAt) const currentTarget = new THREE.Vector3().lerpVectors(startTarget, endTarget, t); camera.lookAt(currentTarget); // Rotate terrain slightly based on scroll to add dynamism terrain.rotation.z = scrollPercent * Math.PI * 0.1; // Gentle turn } // Scroll Event (using raw scroll for simplicity, though GSAP ScrollTrigger is smoother) window.addEventListener('scroll', () => { scrollPercent = window.scrollY / totalHeight; }); // Mouse Interaction (Parallax) let mouseX = 0; let mouseY = 0; window.addEventListener('mousemove', (e) => { mouseX = (e.clientX / window.innerWidth) * 2 - 1; mouseY = -(e.clientY / window.innerHeight) * 2 + 1; }); // --- Animation Loop --- const clock = new THREE.Clock(); function animate() { const time = clock.getElapsedTime(); // Subtle particle movement particlesMesh.rotation.y = time * 0.02; const positions = particlesGeo.attributes.position.array; for(let i=1; i<positions.length; i+=3) { positions[i] += Math.sin(time + positions[i-1]) * 0.02; // Bob up and down } particlesGeo.attributes.position.needsUpdate = true; // Subtle terrain wave (simulate heat haze or flowing sand - slow) terrain.position.y = Math.sin(time * 0.1) * 0.2; // Mouse influence on camera rig (secondary motion) const parallaxX = mouseX * 2; const parallaxY = mouseY * 2; camera.position.x += parallaxX * 0.05; camera.position.y += parallaxY * 0.05; // Call update updateCamera(); renderer.render(scene, camera); requestAnimationFrame(animate); } // --- Responsive --- window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); // --- Init --- // Hide loader after "simulated" init window.onload = () => { setTimeout(() => { document.getElementById('loader').style.opacity = '0'; setTimeout(() => { document.getElementById('loader').style.display = 'none'; }, 1000); }, 1500); animate(); // initial target set camera.lookAt(startTarget); }; // GSAP for flawless scrolling integration (optional enhancement for smoothness) gsap.registerPlugin(ScrollTrigger); // Create a proxy object to sync GSAP scroll with our scrollPercent variable if desired // But the raw scroll + lerp in the loop is already very smooth if we use damping. // Let's stick to the manual implementation to keep strict control over the flight path, // but we can use GSAP to animate sections appearance. gsap.utils.toArray('.content-container').forEach(container => { gsap.from(container, { scrollTrigger: { trigger: container, start: "top 80%", toggleActions: "play none none reverse" }, y: 50, opacity: 0, duration: 1.5, ease: "power3.out" }); }); </script> </body> </html> ```