Merge branch 'main' of git.broken.graphics:alina/alinamarquardt.com

# Conflicts:
#	public_html/functions.js
main
Alina Marquardt 2025-05-16 12:04:36 +02:00
commit 86580c51a9
2 changed files with 56 additions and 79 deletions

View File

@ -8,13 +8,6 @@ document.querySelectorAll('.paused-animation').forEach(el => {
// track players
const players = document.querySelectorAll('.track-player');
function updateAllWaveformWidths() {
players.forEach(player => {
const container = player.querySelector('.track-container');
if (container) update_player_waveform_widths(container);
});
}
document.addEventListener('DOMContentLoaded', updateAllWaveformWidths);
window.addEventListener('resize', updateAllWaveformWidths);
@ -22,7 +15,7 @@ players.forEach(player => {
const trackName = player.getAttribute('data-track');
const button = document.createElement('div');
button.className = 'blurgreen play-button play'; // start as play
button.className = 'blurgreen play-button play';
player.appendChild(button);
const wrapper = document.createElement('div');
@ -38,7 +31,6 @@ players.forEach(player => {
const playercontainer = player.querySelector('.track-container');
const played = player.querySelector('.track-played');
const unplayed = player.querySelector('.track-unplayed');
played.style.backgroundImage = `url('tracks/${trackName}-played.webp')`;
unplayed.style.backgroundImage = `url('tracks/${trackName}.webp')`;
const audio = document.createElement('audio');
@ -46,7 +38,7 @@ players.forEach(player => {
player.appendChild(audio);
button.addEventListener('click', () => {
// Pause other players immediately
// pause other players immediately
players.forEach(p => {
const otherAudio = p.querySelector('audio');
const otherBtn = p.querySelector('.play-button');
@ -59,8 +51,9 @@ players.forEach(player => {
}
});
// Then lazy-load and play/pause current audio as before...
// 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();
@ -93,15 +86,14 @@ players.forEach(player => {
}
});
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(); // Use container rect!
const rect = playercontainer.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const width = rect.width;
@ -112,18 +104,18 @@ players.forEach(player => {
audio.addEventListener('timeupdate', () => {
const portion = audio.duration ? audio.currentTime / audio.duration : 0;
update_play_position(played, unplayed, portion);
updatePlayPosition(played, unplayed, portion);
});
});
function update_play_position(played_element, unplayed_element, 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 update_player_waveform_widths(player_container_element) {
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');
@ -131,51 +123,49 @@ function update_player_waveform_widths(player_container_element) {
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
// Wait for everything to load
window.onload = function() {
// Make sure THREE is available
// Make sure three.js is available
if (typeof THREE === 'undefined') {
console.error('THREE.js is not loaded from CDN');
// Provide visual feedfront in the container
const container = document.getElementById('icosahedron-container');
container.innerHTML = '<div style="color: red; padding: 10px;">THREE.js not loaded</div>';
return;
}
// Initialize the scene
// init scene
const container = document.getElementById('icosahedron-container');
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
// Setup renderer
// setup renderer
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(containerWidth, containerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x000000, 0); // Transparent frontground
renderer.setClearColor(0x000000, 0); // transparent
container.appendChild(renderer.domElement);
// Setup scene
// setup scene and camera
const scene = new THREE.Scene();
// Setup camera
const camera = new THREE.PerspectiveCamera(26, containerWidth / containerHeight, 0.1, 100);
camera.position.z = 4;
// Create icosahedron with manually defined vertices
// icosahedron with manually defined vertices
function createIcosahedron(radius) {
// Golden ratio for icosahedron vertices
// golden ratio
const t = (1 + Math.sqrt(5)) / 2;
// Normalize radius
// normalize radius
const normRadius = radius / Math.sqrt(1 + t * t);
// Create vertices
const vertices = [
[-1, t, 0],
[1, t, 0],
@ -191,7 +181,7 @@ window.onload = function() {
[-t, 0, 1]
].map(v => new THREE.Vector3(v[0] * normRadius, v[1] * normRadius, v[2] * normRadius));
// Define edges (pairs of vertex indices)
// edges (pairs of vertex indices)
const edges = [
[0, 11],
[0, 5],
@ -221,8 +211,7 @@ window.onload = function() {
[6, 10],
[7, 8],
[7, 10],
[8, 9], /*[8, 10],*/
/*[9, 11],*/
[8, 9],
[10, 11]
];
@ -232,7 +221,7 @@ window.onload = function() {
};
}
// Convert vertex indices to actual vertices and create a line
// 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];
@ -248,49 +237,44 @@ window.onload = function() {
return new THREE.Line(geometry, material);
}
// Function to create the dual line rendering (thin back lines, thick front lines)
// create thin back lines, thick front lines
function createDualLineRendering(radius) {
const group = new THREE.Group();
const icosa = createIcosahedron(radius);
// Create materials
const backMaterial = new THREE.MeshBasicMaterial({
color: 0x63a8b8, // green for back lines
depthTest: false, // Don't test depth for back lines
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, // white for front lines
color: 0xffffff,
depthTest: true, // depth test for front lines
transparent: true,
depthTest: true, // Use depth test for front lines
side: THREE.DoubleSide
});
// For each edge, create:
// 1. A thin cylindrical tube with back material that ignores depth
// 2. A cylindrical tube with front material that uses depth testing
const backRadius = 0.025; // thin for back lines
const frontRadius = 0.035; // thick for front lines
// 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]];
// Create back line (thick)
const backLine = createCylinderBetweenPoints(start, end, backRadius, 1, backMaterial);
backLine.renderOrder = 1; // Render after front lines
backLine.renderOrder = 1;
group.add(backLine);
// Create front line (thin)
const frontLine = createCylinderBetweenPoints(start, end, frontRadius, 0.95, frontMaterial);
frontLine.renderOrder = 3; // Render before back lines
frontLine.renderOrder = 3;
group.add(frontLine);
});
// Create faces for depth testing (invisible)
// invisible face material for depth testing
const faceMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
@ -299,7 +283,7 @@ window.onload = function() {
side: THREE.DoubleSide
});
// Create faces for internal icosahedron
// internal icosahedra material
const internalMaterial = new THREE.MeshBasicMaterial({
color: 0x63a8b8,
depthTest: false,
@ -308,7 +292,7 @@ window.onload = function() {
side: THREE.DoubleSide
});
// Define faces of icosahedron (each is a triangle of vertex indices)
// triangles of vertex indices
const faces = [
[0, 11, 5],
[0, 5, 1],
@ -332,7 +316,7 @@ window.onload = function() {
[9, 8, 1]
];
// Create invisible faces for depth testing
// create depth testing and internal icosahedra
faces.forEach(face => {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array([
@ -345,50 +329,46 @@ window.onload = function() {
const faceMesh = new THREE.Mesh(geometry, faceMaterial);
faceMesh.scale.set(0.975, 0.975, 0.975);
faceMesh.renderOrder = 0; // Render first for depth buffer
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; // Render between front and back
internalMesh1.renderOrder = 1;
group.add(internalMesh1);
internalMesh2.scale.set(0.65, 0.65, 0.65);
internalMesh2.renderOrder = 1; // Render between front and back
internalMesh2.renderOrder = 1;
group.add(internalMesh2);
internalMesh3.scale.set(0.5, 0.5, 0.5);
internalMesh3.renderOrder = 1; // Render between front and back
internalMesh3.renderOrder = 1;
group.add(internalMesh3);
internalMesh4.scale.set(0.4, 0.4, 0.4);
internalMesh4.renderOrder = 1; // Render between front and back
internalMesh4.renderOrder = 1;
group.add(internalMesh4);
internalMesh5.scale.set(0.3, 0.3, 0.3);
internalMesh5.renderOrder = 1; // Render between front and back
internalMesh5.renderOrder = 1;
group.add(internalMesh5);
});
return group;
}
// Function to create a cylinder between two points
function createCylinderBetweenPoints(pointX, pointY, radius, lengthmultiplier, material) {
// Direction from pointX to pointY
const direction = new THREE.Vector3().subVectors(pointY, pointX);
const length = direction.length();
// Create cylinder
const geometry = new THREE.CylinderGeometry(radius, radius, length * lengthmultiplier, 4, 1);
// By default, cylinder is along Y-axis, so rotate it
// default cylinder is along y-axis, rotate it
geometry.rotateX(Math.PI / 2);
// Create mesh
const cylinder = new THREE.Mesh(geometry, material);
// Position and orient cylinder
// position and orient cylinder
const midpoint = new THREE.Vector3().addVectors(pointX, pointY).multiplyScalar(0.5);
cylinder.position.copy(midpoint);
cylinder.lookAt(pointY);
@ -396,11 +376,11 @@ window.onload = function() {
return cylinder;
}
// Create our icosahedron
// create icosahedron
const icosahedronGroup = createDualLineRendering(0.85);
scene.add(icosahedronGroup);
// Animation state
// animation state
let animating = true;
let lastFrameTime = 0;
@ -408,7 +388,6 @@ window.onload = function() {
const animMultiplier = 30 / targetFPS;
const frameDuration = 1000 / targetFPS;
// Animation function
function animate(now) {
requestAnimationFrame(animate);
@ -417,7 +396,6 @@ window.onload = function() {
lastFrameTime = now;
// Rotate if animation is enabled
if (animating) {
icosahedronGroup.rotation.x -= 0.002 * animMultiplier;
icosahedronGroup.rotation.y += 0.004 * animMultiplier;
@ -427,12 +405,9 @@ window.onload = function() {
renderer.render(scene, camera);
}
// Start animation
animate();
// Handle window resize
window.addEventListener('resize', function() {
// Only update if container dimensions change
const newWidth = container.clientWidth;
const newHeight = container.clientHeight;
@ -443,5 +418,5 @@ window.onload = function() {
}
});
console.log('Icosahedron wireframe created successfully');
// console.log('icosahedron wireframe created successfully');
};

View File

@ -12,6 +12,8 @@
<link rel="stylesheet" href="style.css" fetchpriority="high" />
<link rel="me" href="https://tech.lgbt/@0" />
<link rel="preload" as="image" type="image/webp" href="img/bg.webp" fetchpriority="high" />
<link rel="preload" as="image" type="image/webp" href="img/bg_blur.webp" />
<link rel="preload" as="image" type="image/svg+xml" href="img/track-pause.svg" />
@ -141,7 +143,7 @@
<div class="paused-animation" style="aspect-ratio: 900 / 528;">
<div class="play-btn"></div>
<img class="poster-frame" src="img/experiments/lf-poster.webp" width="680" height="400" alt="LF 3d animation poster-frame" />
<img class="anim" src="img/experiments/lf-anim.gif" width="680" height="400" alt="LF 3d animation" />
<img class="anim" src="img/experiments/lf-anim.gif" width="680" height="400" alt="LF 3d animation" loading="lazy" />
</div>
<p>a specific animation style I had in mind for my music persona lastfuture required objects with transparent flat faces, integer snapping, controlled image noise and visual artefacts. to solve this I wrote the tiny 3d engine <span class="nowrap"><a href="https://git.broken.graphics/alina/3d-simple/" target="_blank">3d simple</a></span> that can create this animation style then render it out as a gif.</p>
</article>