Custom Code Experiment •
Metamorphosis 1
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>Metamorphosis 1</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #111;
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(20, 20, 20, 0.85);
padding: 20px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
user-select: none;
}
h1 {
margin: 0 0 10px 0;
font-size: 1.2rem;
}
p {
margin: 0 0 20px 0;
font-size: 0.9rem;
color: #aaa;
}
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: #333;
border-radius: 4px;
}
input[type=range]::-webkit-slider-thumb {
height: 24px;
width: 24px;
border-radius: 50%;
background: #e0ca45;
cursor: pointer;
-webkit-appearance: none;
margin-top: -8px;
box-shadow: 0 0 10px rgba(224, 202, 69, 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>
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x111111, 0.015);
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 30, 45);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(20, 40, 20);
scene.add(dirLight);
const fillLight = new THREE.DirectionalLight(0xaaccff, 0.4);
fillLight.position.set(-20, 20, -20);
scene.add(fillLight);
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#dfdfdf';
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = '#222222';
ctx.fillRect(0, 0, 256, 256);
ctx.fillRect(256, 256, 256, 256);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
const gridSize = 14;
const resolution = 12;
const segments = gridSize * resolution;
const planeSize = 60;
texture.repeat.set(gridSize / 2, gridSize / 2);
const geometry = new THREE.PlaneGeometry(planeSize, planeSize, segments, segments);
geometry.rotateX(-Math.PI / 2);
const basePositions = new Float32Array(geometry.attributes.position.array);
const material = new THREE.MeshStandardMaterial({
map: texture,
roughness: 0.5,
metalness: 0.1,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const slider = document.getElementById('morphSlider');
let targetShift = 0;
let currentShift = 0;
slider.addEventListener('input', (e) => {
targetShift = parseFloat(e.target.value);
});
function animate() {
requestAnimationFrame(animate);
currentShift += (targetShift - currentShift) * 0.1;
const pos = geometry.attributes.position.array;
const tileSize = planeSize / gridSize;
for (let i = 0; i < pos.length; i += 3) {
let x = basePositions[i];
let y = basePositions[i + 1];
let z = basePositions[i + 2];
let gx = (x + planeSize / 2) / tileSize;
let gz = (z + planeSize / 2) / tileSize;
let morphProgress = (x + planeSize / 2) / planeSize;
morphProgress += currentShift;
let morph = Math.max(0, Math.min(1, morphProgress));
morph = morph * morph * (3 - 2 * morph);
let dx = Math.sin(gz * Math.PI) * 0.4 + Math.sin(gz * Math.PI * 2.0) * 0.15;
let dz = -Math.sin(gx * Math.PI) * 0.4 - Math.sin(gx * Math.PI * 2.0) * 0.15;
pos[i] = x + (dx * tileSize * morph);
pos[i + 2] = z + (dz * tileSize * morph);
let cx = Math.abs((gx % 1.0) - 0.5);
let cz = Math.abs((gz % 1.0) - 0.5);
let bulge = Math.max(0, 1.0 - (cx + cz) * 2.0);
pos[i + 1] = y + (bulge * tileSize * 0.4 * morph);
}
geometry.attributes.position.needsUpdate = true;
geometry.computeVertexNormals();
controls.update();
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>