// gif player document.querySelectorAll('.paused-animation').forEach(el => { el.addEventListener('click', () => { el.classList.toggle('active'); }); }); // track players const players = document.querySelectorAll('.track-player'); document.addEventListener('DOMContentLoaded', updateAllWaveformWidths); window.addEventListener('resize', updateAllWaveformWidths); players.forEach(player => { const trackName = player.getAttribute('data-track'); const button = document.createElement('div'); button.className = 'blurgreen play-button play'; player.appendChild(button); const wrapper = document.createElement('div'); wrapper.className = 'track'; wrapper.innerHTML = `
`; player.appendChild(wrapper); const playercontainer = player.querySelector('.track-container'); const played = player.querySelector('.track-played'); const unplayed = player.querySelector('.track-unplayed'); unplayed.style.backgroundImage = `url('tracks/${trackName}.webp')`; const audio = document.createElement('audio'); audio.style.display = 'none'; player.appendChild(audio); button.addEventListener('click', () => { // pause other players immediately players.forEach(p => { const otherAudio = p.querySelector('audio'); const otherBtn = p.querySelector('.play-button'); if (otherAudio !== audio) { if (!otherAudio.paused) otherAudio.pause(); if (otherBtn) { otherBtn.classList.add('play'); otherBtn.classList.remove('pause'); } } }); // player needs mp3 url (and played waveform image to lazyload) if (!audio.src) { played.style.backgroundImage = `url('tracks/${trackName}-played.webp')`; audio.src = 'tracks/' + trackName + '.mp3'; audio.currentTime = 0; audio.load(); audio.addEventListener('canplay', () => { audio.play().then(() => { button.classList.remove('play'); button.classList.add('pause'); }).catch(() => { button.classList.add('play'); button.classList.remove('pause'); }); }, { once: true }); return; } if (audio.paused) { audio.play().then(() => { button.classList.remove('play'); button.classList.add('pause'); }).catch(() => { button.classList.add('play'); button.classList.remove('pause'); }); } else { audio.pause(); button.classList.add('play'); button.classList.remove('pause'); } }); audio.addEventListener('ended', () => { button.classList.add('play'); button.classList.remove('pause'); }); // skip to playback position on waveform interaction playercontainer.addEventListener('click', e => { const rect = playercontainer.getBoundingClientRect(); const clickX = e.clientX - rect.left; const width = rect.width; const portion = Math.min(Math.max(clickX / width, 0), 1); audio.currentTime = portion * audio.duration; }); audio.addEventListener('timeupdate', () => { const portion = audio.duration ? audio.currentTime / audio.duration : 0; updatePlayPosition(played, unplayed, portion); }); }); function updatePlayPosition(played_element, unplayed_element, portion) { const playedPercent = portion * 100; const unplayedPercent = 100 - playedPercent; played_element.style.width = playedPercent + "%"; unplayed_element.style.width = unplayedPercent + "%"; } function updatePlayerWaveformWidths(player_container_element) { const trackWidth = player_container_element.offsetWidth; const played_element = player_container_element.querySelector('.track-played'); const unplayed_element = player_container_element.querySelector('.track-unplayed'); played_element.style.backgroundSize = trackWidth + "px 80px"; unplayed_element.style.backgroundSize = trackWidth + "px 80px"; } function updateAllWaveformWidths() { // required for proper display of played/unplayed waveforms players.forEach(player => { const container = player.querySelector('.track-container'); if (container) updatePlayerWaveformWidths(container); }); } // icosahedron window.onload = function() { // Make sure three.js is available if (typeof THREE === 'undefined') { console.error('THREE.js is not loaded from CDN'); return; } // init scene const container = document.getElementById('icosahedron-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; // setup renderer const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(containerWidth, containerHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.setClearColor(0x000000, 0); // transparent container.appendChild(renderer.domElement); // setup scene and camera const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(26, containerWidth / containerHeight, 0.1, 100); camera.position.z = 4; // icosahedron with manually defined vertices function createIcosahedron(radius) { // golden ratio const t = (1 + Math.sqrt(5)) / 2; // normalize radius const normRadius = radius / Math.sqrt(1 + t * t); const vertices = [ [-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0], [0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t], [t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1] ].map(v => new THREE.Vector3(v[0] * normRadius, v[1] * normRadius, v[2] * normRadius)); // edges (pairs of vertex indices) const edges = [ [0, 11], [0, 5], [0, 1], [0, 7], [0, 10], [1, 5], [1, 7], [1, 8], [1, 9], [2, 3], [2, 4], [2, 6], [2, 10], [2, 11], [3, 4], [3, 6], [3, 8], [3, 9], [4, 5], [4, 9], [4, 11], [5, 9], [5, 11], [6, 7], [6, 8], [6, 10], [7, 8], [7, 10], [8, 9], [10, 11] ]; return { vertices: vertices, edges: edges }; } // convert vertex indices to actual vertices and create a line function createLine(icosa, edgeIndex, material) { const startIndex = icosa.edges[edgeIndex][0]; const endIndex = icosa.edges[edgeIndex][1]; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array([ icosa.vertices[startIndex].x, icosa.vertices[startIndex].y, icosa.vertices[startIndex].z, icosa.vertices[endIndex].x, icosa.vertices[endIndex].y, icosa.vertices[endIndex].z ]); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); return new THREE.Line(geometry, material); } // create thin back lines, thick front lines function createDualLineRendering(radius) { const group = new THREE.Group(); const icosa = createIcosahedron(radius); const backMaterial = new THREE.MeshBasicMaterial({ color: 0x63a8b8, depthTest: false, // don't test depth for back lines transparent: true, opacity: 0.4, side: THREE.DoubleSide }); const frontMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, depthTest: true, // depth test for front lines transparent: true, side: THREE.DoubleSide }); // cylinders for each edge const backRadius = 0.025; const frontRadius = 0.035; icosa.edges.forEach((edge, index) => { const start = icosa.vertices[edge[0]]; const end = icosa.vertices[edge[1]]; const backLine = createCylinderBetweenPoints(start, end, backRadius, 1, backMaterial); backLine.renderOrder = 1; group.add(backLine); const frontLine = createCylinderBetweenPoints(start, end, frontRadius, 0.95, frontMaterial); frontLine.renderOrder = 3; group.add(frontLine); }); // invisible face material for depth testing const faceMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0, depthWrite: true, side: THREE.DoubleSide }); // internal icosahedra material const internalMaterial = new THREE.MeshBasicMaterial({ color: 0x63a8b8, depthTest: false, transparent: true, opacity: 0.2, side: THREE.DoubleSide }); // triangles of vertex indices const faces = [ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1] ]; // create depth testing and internal icosahedra faces.forEach(face => { const geometry = new THREE.BufferGeometry(); const positions = new Float32Array([ icosa.vertices[face[0]].x, icosa.vertices[face[0]].y, icosa.vertices[face[0]].z, icosa.vertices[face[1]].x, icosa.vertices[face[1]].y, icosa.vertices[face[1]].z, icosa.vertices[face[2]].x, icosa.vertices[face[2]].y, icosa.vertices[face[2]].z ]); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const faceMesh = new THREE.Mesh(geometry, faceMaterial); faceMesh.scale.set(0.975, 0.975, 0.975); faceMesh.renderOrder = 0; group.add(faceMesh); // stack internal icosahedra for volumetric effect const internalMesh1 = new THREE.Mesh(geometry, internalMaterial); const internalMesh2 = new THREE.Mesh(geometry, internalMaterial); const internalMesh3 = new THREE.Mesh(geometry, internalMaterial); const internalMesh4 = new THREE.Mesh(geometry, internalMaterial); const internalMesh5 = new THREE.Mesh(geometry, internalMaterial); internalMesh1.scale.set(0.85, 0.85, 0.85); internalMesh1.renderOrder = 1; group.add(internalMesh1); internalMesh2.scale.set(0.65, 0.65, 0.65); internalMesh2.renderOrder = 1; group.add(internalMesh2); internalMesh3.scale.set(0.5, 0.5, 0.5); internalMesh3.renderOrder = 1; group.add(internalMesh3); internalMesh4.scale.set(0.4, 0.4, 0.4); internalMesh4.renderOrder = 1; group.add(internalMesh4); internalMesh5.scale.set(0.3, 0.3, 0.3); internalMesh5.renderOrder = 1; group.add(internalMesh5); }); return group; } function createCylinderBetweenPoints(pointX, pointY, radius, lengthmultiplier, material) { const direction = new THREE.Vector3().subVectors(pointY, pointX); const length = direction.length(); const geometry = new THREE.CylinderGeometry(radius, radius, length * lengthmultiplier, 4, 1); // default cylinder is along y-axis, rotate it geometry.rotateX(Math.PI / 2); const cylinder = new THREE.Mesh(geometry, material); // position and orient cylinder const midpoint = new THREE.Vector3().addVectors(pointX, pointY).multiplyScalar(0.5); cylinder.position.copy(midpoint); cylinder.lookAt(pointY); return cylinder; } // create icosahedron const icosahedronGroup = createDualLineRendering(0.85); scene.add(icosahedronGroup); // animation state let animating = true; let lastFrameTime = 0; const targetFPS = 20; const animMultiplier = 30 / targetFPS; const frameDuration = 1000 / targetFPS; function animate(now) { requestAnimationFrame(animate); const delta = now - lastFrameTime; if (delta < frameDuration) return; lastFrameTime = now; if (animating) { icosahedronGroup.rotation.x -= 0.002 * animMultiplier; icosahedronGroup.rotation.y += 0.004 * animMultiplier; icosahedronGroup.rotation.z -= 0.001 * animMultiplier; } renderer.render(scene, camera); } animate(); window.addEventListener('resize', function() { const newWidth = container.clientWidth; const newHeight = container.clientHeight; if (newWidth !== containerWidth || newHeight !== containerHeight) { camera.aspect = newWidth / newHeight; camera.updateProjectionMatrix(); renderer.setSize(newWidth, newHeight); } }); // console.log('icosahedron wireframe created successfully'); };