297 lines
11 KiB
HTML
297 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Three.js - Interactive Crystal Garden</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#info {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
color: white;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
max-width: 400px;
|
|
z-index: 100;
|
|
}
|
|
#info h2 {
|
|
margin: 0 0 10px 0;
|
|
font-size: 18px;
|
|
}
|
|
#info .web-source {
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid rgba(255,255,255,0.3);
|
|
font-size: 12px;
|
|
opacity: 0.8;
|
|
}
|
|
#controls-hint {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
color: white;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
padding: 10px 20px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="info">
|
|
<h2>Interactive Crystal Garden</h2>
|
|
<p><strong>Technique:</strong> OrbitControls for immersive 3D exploration</p>
|
|
<p><strong>Learning:</strong> Implemented smooth camera controls with damping, zoom limits, and rotation constraints to create an intuitive exploration experience of a procedurally generated crystal formation.</p>
|
|
<div class="web-source">
|
|
<strong>Web Source:</strong><br>
|
|
<a href="https://threejs.org/docs/examples/en/controls/OrbitControls.html" target="_blank" style="color: #4fc3f7;">Three.js OrbitControls Documentation</a><br>
|
|
<em>Applied: enableDamping (0.05), zoom limits (5-50), polar angle constraints (Math.PI/6 to Math.PI/2), and autoRotate for subtle animation</em>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="controls-hint">
|
|
🖱️ Left Click + Drag: Rotate | Right Click + Drag: Pan | Scroll: Zoom
|
|
</div>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
// Scene setup
|
|
let camera, scene, renderer, controls;
|
|
let crystals = [];
|
|
let time = 0;
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
// Camera setup - positioned to view the crystal garden from an interesting angle
|
|
camera = new THREE.PerspectiveCamera(
|
|
60,
|
|
window.innerWidth / window.innerHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
camera.position.set(15, 12, 15);
|
|
|
|
// Scene with dark background to make crystals pop
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x0a0a1a);
|
|
scene.fog = new THREE.Fog(0x0a0a1a, 30, 60);
|
|
|
|
// Renderer with antialiasing for smooth edges
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// OrbitControls setup - This is the core learning from the web source
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
|
|
// Enable damping for smooth, inertial movement
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
|
|
// Set zoom limits for optimal viewing experience
|
|
controls.minDistance = 5;
|
|
controls.maxDistance = 50;
|
|
|
|
// Constrain vertical rotation to prevent upside-down views
|
|
controls.minPolarAngle = Math.PI / 6; // 30 degrees from top
|
|
controls.maxPolarAngle = Math.PI / 2; // 90 degrees (horizon)
|
|
|
|
// Enable subtle auto-rotation for dynamic presentation
|
|
controls.autoRotate = true;
|
|
controls.autoRotateSpeed = 0.5;
|
|
|
|
// Set target to center of scene
|
|
controls.target.set(0, 3, 0);
|
|
|
|
// Update controls after manual camera positioning
|
|
controls.update();
|
|
|
|
// Create the crystal garden visualization
|
|
createCrystalGarden();
|
|
|
|
// Lighting setup
|
|
createLighting();
|
|
|
|
// Handle resize
|
|
window.addEventListener('resize', onWindowResize);
|
|
}
|
|
|
|
function createCrystalGarden() {
|
|
// Create ground plane
|
|
const groundGeometry = new THREE.CircleGeometry(25, 64);
|
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x1a1a2e,
|
|
roughness: 0.8,
|
|
metalness: 0.2
|
|
});
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
ground.rotation.x = -Math.PI / 2;
|
|
ground.receiveShadow = true;
|
|
scene.add(ground);
|
|
|
|
// Create multiple crystal clusters at different positions
|
|
const clusterPositions = [
|
|
{ x: 0, z: 0, count: 12, radius: 4 },
|
|
{ x: -8, z: 6, count: 8, radius: 3 },
|
|
{ x: 10, z: -4, count: 6, radius: 2.5 },
|
|
{ x: -6, z: -8, count: 7, radius: 2.8 },
|
|
{ x: 8, z: 8, count: 9, radius: 3.2 }
|
|
];
|
|
|
|
clusterPositions.forEach(cluster => {
|
|
createCrystalCluster(cluster.x, cluster.z, cluster.count, cluster.radius);
|
|
});
|
|
}
|
|
|
|
function createCrystalCluster(centerX, centerZ, count, radius) {
|
|
const colors = [
|
|
0x4fc3f7, // Cyan
|
|
0x9c27b0, // Purple
|
|
0xff6b6b, // Pink
|
|
0x4ecdc4, // Teal
|
|
0xf06292 // Rose
|
|
];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
// Random position within cluster radius
|
|
const angle = (i / count) * Math.PI * 2 + Math.random() * 0.5;
|
|
const dist = Math.random() * radius;
|
|
const x = centerX + Math.cos(angle) * dist;
|
|
const z = centerZ + Math.sin(angle) * dist;
|
|
|
|
// Random crystal height and size
|
|
const height = 2 + Math.random() * 6;
|
|
const baseRadius = 0.3 + Math.random() * 0.5;
|
|
|
|
// Create octahedron for crystal shape (two pyramids)
|
|
const geometry = new THREE.OctahedronGeometry(baseRadius, 0);
|
|
|
|
// Scale vertically to create elongated crystal
|
|
geometry.scale(1, height / baseRadius, 1);
|
|
|
|
// Random color from palette
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
const material = new THREE.MeshPhysicalMaterial({
|
|
color: color,
|
|
metalness: 0.3,
|
|
roughness: 0.1,
|
|
transparent: true,
|
|
opacity: 0.85,
|
|
envMapIntensity: 1.0,
|
|
clearcoat: 1.0,
|
|
clearcoatRoughness: 0.1
|
|
});
|
|
|
|
const crystal = new THREE.Mesh(geometry, material);
|
|
crystal.position.set(x, height / 2, z);
|
|
|
|
// Random rotation for variety
|
|
crystal.rotation.y = Math.random() * Math.PI * 2;
|
|
crystal.rotation.z = (Math.random() - 0.5) * 0.2;
|
|
|
|
crystal.castShadow = true;
|
|
crystal.receiveShadow = true;
|
|
|
|
scene.add(crystal);
|
|
crystals.push({
|
|
mesh: crystal,
|
|
baseY: height / 2,
|
|
floatSpeed: 0.5 + Math.random() * 0.5,
|
|
floatOffset: Math.random() * Math.PI * 2
|
|
});
|
|
}
|
|
}
|
|
|
|
function createLighting() {
|
|
// Ambient light for base illumination
|
|
const ambientLight = new THREE.AmbientLight(0x404080, 0.3);
|
|
scene.add(ambientLight);
|
|
|
|
// Main directional light (simulating sun/moon)
|
|
const mainLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
mainLight.position.set(10, 20, 10);
|
|
mainLight.castShadow = true;
|
|
mainLight.shadow.camera.left = -20;
|
|
mainLight.shadow.camera.right = 20;
|
|
mainLight.shadow.camera.top = 20;
|
|
mainLight.shadow.camera.bottom = -20;
|
|
mainLight.shadow.mapSize.width = 2048;
|
|
mainLight.shadow.mapSize.height = 2048;
|
|
scene.add(mainLight);
|
|
|
|
// Accent lights for crystal illumination
|
|
const accentLight1 = new THREE.PointLight(0x4fc3f7, 1.5, 20);
|
|
accentLight1.position.set(-8, 5, 6);
|
|
scene.add(accentLight1);
|
|
|
|
const accentLight2 = new THREE.PointLight(0xff6b6b, 1.5, 20);
|
|
accentLight2.position.set(10, 5, -4);
|
|
scene.add(accentLight2);
|
|
|
|
const accentLight3 = new THREE.PointLight(0x9c27b0, 1.5, 20);
|
|
accentLight3.position.set(0, 8, 0);
|
|
scene.add(accentLight3);
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
time += 0.01;
|
|
|
|
// Subtle floating animation for crystals
|
|
crystals.forEach(crystal => {
|
|
crystal.mesh.position.y = crystal.baseY +
|
|
Math.sin(time * crystal.floatSpeed + crystal.floatOffset) * 0.1;
|
|
|
|
// Gentle rotation
|
|
crystal.mesh.rotation.y += 0.002;
|
|
});
|
|
|
|
// CRITICAL: Update controls in animation loop
|
|
// Required when enableDamping or autoRotate are true
|
|
controls.update();
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|