|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>MS1-V</title> |
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500&display=swap" rel="stylesheet"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
body { |
|
overflow: hidden; |
|
background: #000; |
|
font-family: 'Orbitron', sans-serif; |
|
} |
|
canvas { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
} |
|
.title-screen { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background: #000; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
flex-direction: column; |
|
color: #0ff; |
|
text-align: center; |
|
z-index: 20; |
|
transition: opacity 1s ease-in-out; |
|
} |
|
.title-screen h1 { |
|
font-size: 5em; |
|
margin-bottom: 20px; |
|
} |
|
.title-screen button { |
|
padding: 15px 30px; |
|
background: rgba(0, 255, 255, 0.2); |
|
border: 2px solid #0ff; |
|
color: #0ff; |
|
cursor: pointer; |
|
font-size: 1.2em; |
|
border-radius: 10px; |
|
transition: all 0.3s; |
|
} |
|
.title-screen button:hover { |
|
background: #0ff; |
|
color: #000; |
|
} |
|
.interface { |
|
position: fixed; |
|
top: 20px; |
|
left: 20px; |
|
background: rgba(0, 0, 0, 0.8); |
|
border: 2px solid #0ff; |
|
padding: 20px; |
|
color: #0ff; |
|
z-index: 10; |
|
display: none; |
|
border-radius: 15px; |
|
backdrop-filter: blur(5px); |
|
} |
|
.controls { |
|
margin-top: 15px; |
|
display: flex; |
|
gap: 10px; |
|
} |
|
.controls button { |
|
flex: 1; |
|
padding: 12px; |
|
background: rgba(0, 255, 255, 0.2); |
|
border: 1px solid #0ff; |
|
color: #0ff; |
|
cursor: pointer; |
|
transition: all 0.3s; |
|
border-radius: 8px; |
|
text-transform: uppercase; |
|
letter-spacing: 1px; |
|
} |
|
.controls button:hover { |
|
background: #0ff; |
|
color: #000; |
|
} |
|
.toggle-button { |
|
position: fixed; |
|
top: 20px; |
|
right: 20px; |
|
padding: 12px 20px; |
|
background: rgba(0, 255, 255, 0.2); |
|
color: #0ff; |
|
border: 1px solid #0ff; |
|
cursor: pointer; |
|
z-index: 10; |
|
border-radius: 8px; |
|
text-transform: uppercase; |
|
letter-spacing: 1px; |
|
} |
|
.toggle-button:hover { |
|
background: #0ff; |
|
color: #000; |
|
} |
|
.track-list { |
|
margin-top: 20px; |
|
} |
|
.track-list select { |
|
padding: 10px; |
|
background: rgba(0, 255, 255, 0.2); |
|
border: 1px solid #0ff; |
|
color: #0ff; |
|
border-radius: 8px; |
|
width: 100%; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="title-screen" id="titleScreen"> |
|
<h1>MS1-V</h1> |
|
<button id="startButton">Start</button> |
|
</div> |
|
<button class="toggle-button">Controls</button> |
|
<div class="interface"> |
|
<input type="file" id="audioFile" accept="audio/*"> |
|
<div class="track-list"> |
|
<label for="trackSelect">Select Track:</label> |
|
<select id="trackSelect"> |
|
<option value="">Select a track</option> |
|
<option value="track1.mp3">Track 1</option> |
|
<option value="track2.mp3">Track 2</option> |
|
<option value="track3.mp3">Track 3</option> |
|
</select> |
|
</div> |
|
<div class="controls"> |
|
<button id="play">Play</button> |
|
<button id="pause">Pause</button> |
|
<button id="stop">Stop</button> |
|
</div> |
|
</div> |
|
<script> |
|
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); |
|
document.body.appendChild(renderer.domElement); |
|
|
|
let isDragging = false; |
|
let previousMousePosition = { x: 0, y: 0 }; |
|
let cameraAngle = { x: 0, y: 0 }; |
|
const cameraDistance = 100; |
|
|
|
document.addEventListener('mousedown', (e) => { |
|
isDragging = true; |
|
previousMousePosition = { x: e.clientX, y: e.clientY }; |
|
}); |
|
|
|
document.addEventListener('mousemove', (e) => { |
|
if (!isDragging) return; |
|
|
|
const deltaMove = { |
|
x: e.clientX - previousMousePosition.x, |
|
y: e.clientY - previousMousePosition.y |
|
}; |
|
|
|
cameraAngle.x += deltaMove.y * 0.005; |
|
cameraAngle.y += deltaMove.x * 0.005; |
|
|
|
cameraAngle.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, cameraAngle.x)); |
|
|
|
previousMousePosition = { x: e.clientX, y: e.clientY }; |
|
}); |
|
|
|
document.addEventListener('mouseup', () => { |
|
isDragging = false; |
|
}); |
|
|
|
const lineMaterial = new THREE.LineBasicMaterial({ |
|
color: 0x00ffff, |
|
transparent: true, |
|
opacity: 0.3, |
|
blending: THREE.AdditiveBlending |
|
}); |
|
|
|
function createBassPattern(frequency, intensity) { |
|
const pattern = new THREE.Group(); |
|
const geometry = new THREE.TorusKnotGeometry(10 * intensity, 2, 100, 16); |
|
const material = new THREE.MeshBasicMaterial({ |
|
color: 0x00ffff, |
|
wireframe: true, |
|
transparent: true, |
|
opacity: intensity |
|
}); |
|
const torusKnot = new THREE.Mesh(geometry, material); |
|
pattern.add(torusKnot); |
|
|
|
for(let i = 0; i < 3; i++) { |
|
const sphereGeo = new THREE.SphereGeometry(5 * intensity, 32, 32); |
|
const sphereMat = new THREE.MeshBasicMaterial({ |
|
color: 0x00ffff, |
|
wireframe: true, |
|
transparent: true, |
|
opacity: intensity * 0.5 |
|
}); |
|
const sphere = new THREE.Mesh(sphereGeo, sphereMat); |
|
sphere.position.setFromSphericalCoords( |
|
20 * intensity, |
|
i * Math.PI * 2/3, |
|
frequency * Math.PI |
|
); |
|
pattern.add(sphere); |
|
} |
|
return pattern; |
|
} |
|
|
|
function createParticleSystem(count, size, spread) { |
|
const geometry = new THREE.BufferGeometry(); |
|
const positions = new Float32Array(count * 3); |
|
const colors = new Float32Array(count * 3); |
|
const scales = new Float32Array(count); |
|
|
|
for(let i = 0; i < count * 3; i += 3) { |
|
positions[i] = (Math.random() - 0.5) * spread; |
|
positions[i + 1] = (Math.random() - 0.5) * spread; |
|
positions[i + 2] = (Math.random() - 0.5) * spread; |
|
scales[i/3] = Math.random(); |
|
} |
|
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); |
|
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); |
|
geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1)); |
|
|
|
const material = new THREE.PointsMaterial({ |
|
size: size, |
|
vertexColors: true, |
|
blending: THREE.AdditiveBlending, |
|
transparent: true, |
|
opacity: 0.8, |
|
}); |
|
|
|
const particles = new THREE.Points(geometry, material); |
|
const lines = new THREE.LineSegments(new THREE.BufferGeometry(), lineMaterial); |
|
|
|
return { particles, lines }; |
|
} |
|
|
|
const innerSystem = createParticleSystem(3000, 0.5, 80); |
|
const midSystem = createParticleSystem(4000, 0.3, 150); |
|
const outerSystem = createParticleSystem(5000, 0.2, 200); |
|
const bassSystem = createParticleSystem(2000, 1.0, 100); |
|
|
|
let bassPattern = null; |
|
|
|
[innerSystem, midSystem, outerSystem, bassSystem].forEach(system => { |
|
scene.add(system.particles); |
|
scene.add(system.lines); |
|
}); |
|
|
|
camera.position.z = cameraDistance; |
|
|
|
function updateParticleSystem(system, frequencyData, baseIndex, scale, bass, time) { |
|
const positions = system.particles.geometry.attributes.position.array; |
|
const colors = system.particles.geometry.attributes.color.array; |
|
const scales = system.particles.geometry.attributes.scale.array; |
|
|
|
const evolutionFactor = Math.sin(time * 0.0005) * 0.5 + 0.5; |
|
|
|
const linePositions = []; |
|
const lineIndices = []; |
|
|
|
for(let i = 0; i < positions.length; i += 3) { |
|
const freqIndex = (baseIndex + Math.floor(i/30)) % frequencyData.length; |
|
const frequency = frequencyData[freqIndex] / 255; |
|
|
|
const angle = (i/3) * 0.1 + time * 0.001; |
|
const radius = scale * (0.5 + 0.5 * frequency); |
|
|
|
if(bass > 0.7) { |
|
const bassImpact = Math.pow(bass - 0.7, 2); |
|
positions[i] = Math.cos(angle) * radius * Math.sin(angle * 2 + evolutionFactor) * (1 + bassImpact); |
|
positions[i + 1] = Math.sin(angle) * radius * Math.cos(angle * 3 + evolutionFactor) * (1 + bassImpact); |
|
positions[i + 2] = Math.cos(angle * 2) * radius * Math.sin(angle + evolutionFactor) * (1 + bassImpact); |
|
} else { |
|
positions[i] *= 1 + frequency * 0.02 * (1 + evolutionFactor); |
|
positions[i + 1] *= 1 + frequency * 0.02 * (1 + evolutionFactor); |
|
positions[i + 2] *= 1 + frequency * 0.02 * (1 + evolutionFactor); |
|
} |
|
|
|
const distance = Math.sqrt(positions[i]**2 + positions[i+1]**2 + positions[i+2]**2); |
|
if(distance > scale) { |
|
positions[i] *= scale/distance; |
|
positions[i+1] *= scale/distance; |
|
positions[i+2] *= scale/distance; |
|
} |
|
|
|
colors[i] = Math.sin(frequency * Math.PI + bass + evolutionFactor) * 0.5 + 0.5; |
|
colors[i + 1] = Math.cos(frequency * Math.PI * 0.5 + bass + evolutionFactor) * 0.5 + 0.5; |
|
colors[i + 2] = frequency + bass * 0.5; |
|
|
|
scales[i/3] = frequency * (2 + bass * 2) * (1 + evolutionFactor); |
|
|
|
if(i % 9 === 0) { |
|
linePositions.push(positions[i], positions[i+1], positions[i+2]); |
|
if(linePositions.length > 6) { |
|
lineIndices.push(linePositions.length/3 - 2, linePositions.length/3 - 1); |
|
} |
|
} |
|
} |
|
|
|
const lineGeometry = new THREE.BufferGeometry(); |
|
lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3)); |
|
lineGeometry.setIndex(lineIndices); |
|
system.lines.geometry.dispose(); |
|
system.lines.geometry = lineGeometry; |
|
|
|
system.particles.geometry.attributes.position.needsUpdate = true; |
|
system.particles.geometry.attributes.color.needsUpdate = true; |
|
system.particles.geometry.attributes.scale.needsUpdate = true; |
|
} |
|
|
|
let audioContext, audioAnalyser, audioSource; |
|
let audioElement = new Audio(); |
|
|
|
document.getElementById('audioFile').addEventListener('change', function(event) { |
|
audioElement.src = URL.createObjectURL(event.target.files[0]); |
|
}); |
|
|
|
document.getElementById('trackSelect').addEventListener('change', function(event) { |
|
audioElement.src = event.target.value; |
|
}); |
|
|
|
document.getElementById('startButton').addEventListener('click', () => { |
|
document.getElementById('titleScreen').style.opacity = 0; |
|
setTimeout(() => { |
|
document.getElementById('titleScreen').style.display = 'none'; |
|
}, 1000); |
|
}); |
|
|
|
document.getElementById('play').addEventListener('click', () => audioElement.play()); |
|
document.getElementById('pause').addEventListener('click', () => audioElement.pause()); |
|
document.getElementById('stop').addEventListener('click', () => { |
|
audioElement.pause(); |
|
audioElement.currentTime = 0; |
|
}); |
|
|
|
document.querySelector('.toggle-button').addEventListener('click', () => { |
|
const interfaceDiv = document.querySelector('.interface'); |
|
interfaceDiv.style.display = interfaceDiv.style.display === 'none' ? 'block' : 'none'; |
|
}); |
|
|
|
audioElement.addEventListener('play', () => { |
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
audioSource = audioContext.createMediaElementSource(audioElement); |
|
audioAnalyser = audioContext.createAnalyser(); |
|
audioSource.connect(audioAnalyser); |
|
audioAnalyser.connect(audioContext.destination); |
|
audioAnalyser.fftSize = 2048; |
|
const bufferLength = audioAnalyser.frequencyBinCount; |
|
const dataArray = new Uint8Array(bufferLength); |
|
|
|
function animate() { |
|
requestAnimationFrame(animate); |
|
const time = Date.now(); |
|
audioAnalyser.getByteFrequencyData(dataArray); |
|
|
|
const bass = Math.max(...dataArray.slice(0, 10)) / 255; |
|
const mid = Math.max(...dataArray.slice(10, 100)) / 255; |
|
const treble = Math.max(...dataArray.slice(100, 200)) / 255; |
|
|
|
camera.position.x = cameraDistance * Math.sin(cameraAngle.y) * Math.cos(cameraAngle.x); |
|
camera.position.y = cameraDistance * Math.sin(cameraAngle.x); |
|
camera.position.z = cameraDistance * Math.cos(cameraAngle.y) * Math.cos(cameraAngle.x); |
|
camera.lookAt(scene.position); |
|
|
|
if(bass > 0.7) { |
|
if(!bassPattern) { |
|
bassPattern = createBassPattern(bass, bass - 0.7); |
|
scene.add(bassPattern); |
|
} |
|
bassPattern.scale.set(1 + bass, 1 + bass, 1 + bass); |
|
bassPattern.rotation.x += 0.02; |
|
bassPattern.rotation.y += 0.03; |
|
} else if(bassPattern) { |
|
scene.remove(bassPattern); |
|
bassPattern = null; |
|
} |
|
|
|
updateParticleSystem(innerSystem, dataArray, 0, 80, bass, time); |
|
updateParticleSystem(midSystem, dataArray, 100, 150, bass, time); |
|
updateParticleSystem(outerSystem, dataArray, 200, 200, bass, time); |
|
updateParticleSystem(bassSystem, dataArray.slice(0, 10), 0, 100 * bass, bass, time); |
|
|
|
innerSystem.particles.rotation.y += 0.003 * (1 + bass * 0.5); |
|
midSystem.particles.rotation.x += 0.002 * (1 + mid * 0.5); |
|
outerSystem.particles.rotation.z += 0.001 * (1 + treble * 0.5); |
|
if(bassSystem.particles) { |
|
bassSystem.particles.rotation.y -= 0.005 * (1 + bass); |
|
} |
|
|
|
renderer.render(scene, camera); |
|
} |
|
animate(); |
|
}); |
|
|
|
window.addEventListener('resize', () => { |
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
|