Custom Code Experiment •
Simple Grid
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Grid</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #1a1a1a;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: white;
}
canvas {
display: block;
}
#ui-container {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 80%;
max-width: 600px;
background: rgba(30, 30, 30, 0.85);
padding: 20px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
margin: 0 0 10px 0;
font-size: 1.2rem;
letter-spacing: 1px;
}
p {
margin: 0 0 20px 0;
font-size: 0.9rem;
color: #aaa;
}
/* Stylish, responsive slider */
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: pointer;
background: #4a4a4a;
border-radius: 4px;
}
input[type=range]::-webkit-slider-thumb {
height: 24px;
width: 24px;
border-radius: 50%;
background: #4CAF50;
cursor: pointer;
-webkit-appearance: none;
margin-top: -8px;
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
transition: transform 0.1s;
}
input[type=range]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
</style>
</head>
<body>
<div id="ui-container">
<input type="range" id="morphSlider" min="-1" max="1" step="0.001" value="0">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
// 1. Scene Setup
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x1a1a1a, 0.015);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 15, 25);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Performance optimization
document.body.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 + 0.1; // Don't let camera go too far below ground
// 2. Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);
const backLight = new THREE.DirectionalLight(0x4CAF50, 0.5); // Greenish rim light
backLight.position.set(-10, 5, -10);
scene.add(backLight);
// 3. Custom Morphing Geometry Construction
// We build a single geometry that has two "states" (Base Polygon and Target Bird)
const geometry = new THREE.BufferGeometry();
// State 1: The Base Polygon (A simple Hexagon)
// 7 Vertices: 1 center point, 6 perimeter points
const baseVertices = new Float32Array([
0, 0, 0, // 0: Center
0, 1, 0, // 1: Top
0.866, 0.5, 0, // 2: Top Right
0.866, -0.5, 0, // 3: Bottom Right
0, -1, 0, // 4: Bottom
-0.866, -0.5, 0, // 5: Bottom Left
-0.866, 0.5, 0 // 6: Top Left
]);
// State 2: The Target Figure (A stylized Bird / Chevron)
const birdVertices = new Float32Array([
0, -0.1, 0.2, // 0: Center (Body lifted slightly in 3D)
0, 0.9, 0.5, // 1: Head/Beak protruding forward
1.4, 0.4, -0.2, // 2: Right Wingtip swept back
0.3, -0.2, 0, // 3: Right Wing root
0, -1.2, -0.3, // 4: Tail swept back
-0.3, -0.2, 0, // 5: Left Wing root
-1.4, 0.4, -0.2 // 6: Left Wingtip swept back
]);
// Define the triangles (faces) connecting the vertices
const indices = [
0, 1, 2,
0, 2, 3,
0, 3, 4,
0, 4, 5,
0, 5, 6,
0, 6, 1
];
geometry.setIndex(indices);
geometry.setAttribute('position', new THREE.BufferAttribute(baseVertices, 3));
geometry.computeVertexNormals(); // Lighting needs normals
// Add the Bird shape as a Morph Target processed by the GPU
geometry.morphAttributes.position = [];
geometry.morphAttributes.position[0] = new THREE.BufferAttribute(birdVertices, 3);
// We also need morph normals for correct lighting during the transition
const birdGeometry = new THREE.BufferGeometry();
birdGeometry.setIndex(indices);
birdGeometry.setAttribute('position', new THREE.BufferAttribute(birdVertices, 3));
birdGeometry.computeVertexNormals();
geometry.morphAttributes.normal = [];
geometry.morphAttributes.normal[0] = birdGeometry.attributes.normal;
// 4. Create the Grid of Meshes
const material = new THREE.MeshStandardMaterial({
color: 0x88ccff,
roughness: 0.4,
metalness: 0.1,
side: THREE.DoubleSide
});
const gridSize = 25;
const spacing = 2.2;
const meshes = [];
// Create an invisible group to hold the grid so we can center it
const gridGroup = new THREE.Group();
scene.add(gridGroup);
const offset = (gridSize * spacing) / 2;
for (let x = 0; x < gridSize; x++) {
for (let z = 0; z < gridSize; z++) {
const mesh = new THREE.Mesh(geometry, material);
// Position in grid
mesh.position.x = (x * spacing) - offset;
mesh.position.z = (z * spacing) - offset;
// Lay them flat
mesh.rotation.x = -Math.PI / 2;
// Store their normalized X position (-1 to 1) to calculate morph gradient
mesh.userData.normalizedX = (mesh.position.x / offset);
gridGroup.add(mesh);
meshes.push(mesh);
}
}
// 5. Responsive UI and Animation Logic
const slider = document.getElementById('morphSlider');
// Target shift represents where the user wants the wave to be.
// Current shift is where it actually is (interpolates for smoothness).
let targetShift = parseFloat(slider.value);
let currentShift = targetShift;
// Extremely lightweight event listener
slider.addEventListener('input', (e) => {
targetShift = parseFloat(e.target.value);
});
// 6. Render Loop
function animate() {
requestAnimationFrame(animate);
// Smoothly ease currentShift towards targetShift (Decouples rendering from UI events)
currentShift += (targetShift - currentShift) * 0.1;
// Update the morph influence for every mesh based on its position and the global shift
meshes.forEach(mesh => {
// Gradient logic: calculate how "bird-like" this specific mesh should be
// mesh.userData.normalizedX goes from -1 (left) to 1 (right)
// currentShift pushes this wave back and forth
let morphValue = (mesh.userData.normalizedX + 1.0) / 2.0; // Base gradient 0 to 1
morphValue += currentShift; // Apply user slider shift
// Clamp the value strictly between 0 (Hexagon) and 1 (Bird)
morphValue = Math.max(0, Math.min(1, morphValue));
// Apply the value to the GPU Morph Target
mesh.morphTargetInfluences[0] = morphValue;
// Optional: Add a gentle bobbing motion to the "birds"
if (morphValue > 0.1) {
mesh.position.y = Math.sin(Date.now() * 0.002 + mesh.position.x) * (morphValue * 0.5);
} else {
mesh.position.y = 0;
}
});
controls.update();
renderer.render(scene, camera);
}
// Handle Window Resizing smoothly
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start the loop
animate();
</script>
</body>
</html>