// 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');
};