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