update texts; remove redundant text; close all collapsibles by default; comment out carousel for now

This commit is contained in:
2025-06-13 22:24:14 +02:00
parent 4e99bb16bc
commit 9cc970ea0b
6 changed files with 313 additions and 349 deletions

View File

@@ -15,7 +15,7 @@ section#academia
// region Expertise
.grid.grid-cols-1.gap-6.mb-8(class="md:grid-cols-1")
+Collapsable("academia", academia.expertise.frontendTechnologies, true)
+Collapsable("academia", academia.expertise.frontendTechnologies)
+Collapsable("academia", academia.expertise.devopsAndCloud)
+Collapsable("academia", academia.expertise.backendTechnologies)
+Collapsable("academia", academia.expertise.databaseAndData)

View File

@@ -6,7 +6,7 @@ mixin Collapsable(name, data, open)
details.mb-4(open=isExpanded, name=name)
summary(
class=`rounded-sm transition mb-2 cursor-pointer text-md font-semibold mb-2 cursor-pointer sm:text-lg text-nls-${color} dark:text-nls-${color} focus:outline-none focus:z-10 focus:ring-4 focus:ring-nls-${color} focus:bg-nls-${color} focus:text-black focus:no-underline`,
class=`rounded-sm transition mb-2 cursor-pointer text-md font-semibold mb-2 cursor-pointer sm:text-lg text-nls-${color} dark:text-nls-${color} focus:outline-none focus:z-10 focus:ring-4 focus:ring-nls-${color} focus:bg-nls-${color} focus:text-black dark:focus:text-white focus:no-underline`,
onclick=`umami.track('collapsable clicked', { category: '${category}', visitDuration: getVisitDuration() })`
)= data.summary
ul.list-disc.list-inside.text-slate-600(class="dark:text-stone-300")

View File

@@ -1,4 +1,4 @@
header.bg-white.text-nls-black(class="dark:text-white dark:bg-nls-black")
header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black")
.teaser.p-8.flex.flex-col.items-center.justify-center(class="sm:p-20")
.max-w-3xl.mb-8.relative(class="w-4/5 min-h-[90vh]")
.peer.absolute.bottom-0.left-0.right-0.z-40.text-center.max-w-3xl.center.py-8
@@ -11,348 +11,312 @@ header.bg-white.text-nls-black(class="dark:text-white dark:bg-nls-black")
|
| & #{landingpage.jobTitle[1]}
canvas#aurora-canvas.absolute.z-0.left-0.right-0.mx-auto.transition(class="h-[54vh] w-full")
canvas#aurora-canvas.absolute.z-0.left-0.right-0.bottom-0.top-0.mx-auto.transition(class="h-[100vh] w-full")
script(src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js")
script.
class QuantumAurora {
constructor(canvas) {
class GuillocheCurtain {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
this.time = 0;
this.animationId = null;
this.mouseX = 0.5;
this.mouseY = 0.5;
if (!this.gl) {
console.warn('WebGL not supported');
return;
}
this.initShaders();
this.initBuffers();
this.setupInteraction();
this.resize();
this.render();
}
initShaders() {
const vertexShaderSource = `
attribute vec2 a_position;
varying vec2 v_uv;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_uv = a_position * 0.5 + 0.5;
}
`;
const fragmentShaderSource = `
precision highp float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
varying vec2 v_uv;
// Noise functions for organic aurora movement
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for(int i = 0; i < 6; i++) {
value += amplitude * noise(p * frequency);
amplitude *= 0.5;
frequency *= 2.0;
}
return value;
}
// Magnetic field simulation
vec2 magneticField(vec2 pos, float time) {
vec2 field = vec2(0.0);
// Earth's magnetic field approximation
float lat = (pos.y - 0.5) * 3.14159;
float lon = (pos.x - 0.5) * 6.28318;
// Magnetic dipole field with solar wind interaction
float solarWind = sin(time * 0.1 + pos.x * 2.0) * 0.3 + 0.7;
float magneticStrength = cos(lat) * solarWind;
field.x = sin(lat * 2.0 + time * 0.2) * magneticStrength;
field.y = cos(lat * 3.0 + time * 0.15) * magneticStrength;
return field * 0.5;
}
// Particle trajectory simulation
float particleFlow(vec2 uv, float time) {
vec2 pos = uv;
float intensity = 0.0;
// Simulate charged particle streams
for(int i = 0; i < 8; i++) {
float fi = float(i);
vec2 offset = vec2(sin(fi * 0.5), cos(fi * 0.7)) * 0.1;
vec2 particlePos = pos + offset;
// Particle follows magnetic field lines
vec2 field = magneticField(particlePos, time + fi);
particlePos += field * 0.3;
// Altitude-dependent intensity (atmosphere interaction)
float altitude = 1.0 - abs(particlePos.y - 0.5) * 2.0;
altitude = smoothstep(0.2, 0.8, altitude);
// Particle stream noise
float streamNoise = fbm(particlePos * 8.0 + vec2(time * 0.3, 0.0));
intensity += altitude * streamNoise * 0.125;
}
return intensity;
}
// Guilloche pattern generation
float guilloche(vec2 uv, float time) {
vec2 pos = uv * 20.0;
// Multiple rotating circles creating interference patterns
float g = 0.0;
// Primary guilloche rings
for(int i = 0; i < 4; i++) {
float fi = float(i);
float radius = 3.0 + fi * 2.0;
float speed = 0.5 + fi * 0.2;
float phase = fi * 1.5708; // π/2 phase offset
vec2 center = vec2(
sin(time * speed + phase) * 2.0,
cos(time * speed * 0.7 + phase) * 1.5
);
float dist = length(pos - center);
g += sin(dist * radius - time * (speed * 10.0 + 5.0)) / (radius * 0.5);
}
// Secondary interference pattern
float interference = sin(pos.x * 3.0 + time) * sin(pos.y * 2.0 + time * 1.3);
g += interference * 0.3;
return g;
}
// Aurora curtain effect
vec3 auroraColor(vec2 uv, float time) {
// Particle flow intensity
float particles = particleFlow(uv, time);
// Guilloche modulation
float guillochePattern = guilloche(uv, time * 0.5);
float guillocheModulation = sin(guillochePattern * 0.5) * 0.5 + 0.5;
// Combine particle physics with guilloche aesthetics
float intensity = particles * (0.7 + guillocheModulation * 0.6);
// Atmospheric layers - different altitudes emit different colors
float altitude = uv.y;
// Oxygen emissions (green/red) - 557.7nm, 630.0nm
vec3 oxygen_green = vec3(0.3, 0.9, 0.2);
vec3 oxygen_red = vec3(0.8, 0.2, 0.1);
// Nitrogen emissions (blue/purple) - 427.8nm, 391.4nm
vec3 nitrogen_blue = vec3(0.2, 0.4, 0.9);
vec3 nitrogen_purple = vec3(0.6, 0.2, 0.8);
// Helium (rare, yellow) - 587.6nm
vec3 helium_yellow = vec3(0.9, 0.8, 0.2);
// Altitude-based color mixing (realistic atmospheric chemistry)
vec3 color = vec3(0.0);
// High altitude (200-400km) - mostly oxygen red
float highAlt = smoothstep(0.7, 0.9, altitude);
color += oxygen_red * highAlt;
// Mid altitude (100-200km) - oxygen green dominates
float midAlt = smoothstep(0.3, 0.7, altitude) * smoothstep(0.7, 0.5, altitude);
color += oxygen_green * midAlt * 2.0;
// Lower altitude (80-100km) - nitrogen blue/purple
float lowAlt = smoothstep(0.0, 0.4, altitude) * smoothstep(0.6, 0.3, altitude);
color += mix(nitrogen_blue, nitrogen_purple, sin(time * 0.3 + uv.x * 5.0) * 0.5 + 0.5) * lowAlt;
// Rare helium emissions (very sparse)
float heliumChance = noise(uv * 100.0 + time * 0.1);
if(heliumChance > 0.95) {
color += helium_yellow * 0.3;
}
// Solar wind interaction - color temperature shift
float solarActivity = sin(time * 0.05) * 0.3 + 0.7;
color *= solarActivity;
// Magnetic field strength affects brightness
vec2 magneticF = magneticField(uv, time);
float magneticIntensity = length(magneticF);
return color * intensity * (0.5 + magneticIntensity);
}
// Atmospheric scattering simulation
vec3 atmosphericScattering(vec3 auroraColor, vec2 uv) {
float atmosphereDensity = exp(-abs(uv.y - 0.5) * 8.0);
// Rayleigh scattering (blue light scattered more)
vec3 rayleigh = vec3(0.1, 0.2, 0.6) * atmosphereDensity * 0.1;
// Mie scattering (dust/ice crystals)
vec3 mie = vec3(0.8, 0.8, 0.9) * atmosphereDensity * 0.05;
return auroraColor + rayleigh + mie;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
// Mouse interaction - simulates solar wind direction
vec2 solarWind = (u_mouse - 0.5) * 0.5;
uv += solarWind * 0.1;
// Generate aurora
vec3 aurora = auroraColor(uv, u_time);
// Apply atmospheric effects
aurora = atmosphericScattering(aurora, uv);
// Subtle animation shimmer
float shimmer = sin(u_time * 2.0 + uv.x * 10.0 + uv.y * 15.0) * 0.05 + 0.95;
aurora *= shimmer;
// Transparency based on intensity
float alpha = length(aurora) * 0.8;
alpha = smoothstep(0.0, 0.3, alpha);
gl_FragColor = vec4(aurora, alpha);
}
`;
this.program = this.createProgram(vertexShaderSource, fragmentShaderSource);
this.gl.useProgram(this.program);
this.u_time = this.gl.getUniformLocation(this.program, 'u_time');
this.u_resolution = this.gl.getUniformLocation(this.program, 'u_resolution');
this.u_mouse = this.gl.getUniformLocation(this.program, 'u_mouse');
}
createProgram(vertexSource, fragmentSource) {
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
const program = this.gl.createProgram();
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.error('Program link error:', this.gl.getProgramInfoLog(program));
return null;
}
return program;
}
createShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error('Shader compile error:', this.gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
initBuffers() {
const vertices = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]);
this.buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
const a_position = this.gl.getAttribLocation(this.program, 'a_position');
this.gl.enableVertexAttribArray(a_position);
this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 0, 0);
}
setupInteraction() {
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
this.mouseX = e.clientX / rect.width;
this.mouseY = 1.0 - (e.clientY / rect.height);
this.options = {
// Visual parameters
spread: 0.25, // Main spread factor (was 0.08)
lineWidth: 0.8, // Line thickness
opacity: 0.6, // Base opacity
splineCount: 40, // Lines per group
groupCount: 6, // Number of spline groups
// Animation parameters
animationSpeed: 0.005, // Global time multiplier
nodeCount: 7, // Attractor nodes
nodeSpeed: {min: 0.3, max: 0.8},
// Color parameters
hueBase: 0.55, // Base hue (cyan-ish)
hueVariation: 0.1, // Color spread
saturation: {min: 0.3, max: 0.7},
lightness: {min: 0.25, max: 0.65},
// Spline parameters
segments: 100, // Resolution per spline
offset: 5, // General offset between splines
startOffset: 0.0, // Y-offset at spline start
endOffset: 0.0, // Y-offset at spline end
offsetTransition: 'smooth', // 'linear', 'smooth', 'sine'
canvasExtension: 1.0, // How far beyond canvas borders (-1.0 means 50% extra on each side)
// Wave complexity
waveFrequencies: [4, 8, 16], // Multiple harmonics
waveAmplitudes: [0.4, 0.25, 0.15], // Corresponding amplitudes
...options
};
this.scene = new THREE.Scene();
this.camera = new THREE.OrthographicCamera(-2, 2, 1.5, -1.5, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({
canvas: canvas,
alpha: true,
antialias: true
});
this.time = 0;
this.splineGroups = [];
this.nodes = [];
this.init();
this.createNodes();
this.createSplines();
this.animate();
}
init() {
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight);
this.renderer.setClearColor(0x000000, 0);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.camera.position.z = 1;
}
createNodes() {
for (let i = 0; i < this.options.nodeCount; i++) {
this.nodes.push({
x: (Math.random() - 0.5) * 3,
y: (Math.random() - 0.5) * 2,
phase: Math.random() * Math.PI * 2,
radius: 0.2 + Math.random() * 0.3,
speed: this.options.nodeSpeed.min +
Math.random() * (this.options.nodeSpeed.max - this.options.nodeSpeed.min)
});
}
}
// Calculate offset transition from start to end
getOffsetTransition(t) {
const {startOffset, endOffset, offsetTransition} = this.options;
switch (offsetTransition) {
case 'linear':
return startOffset + (endOffset - startOffset) * t;
case 'sine':
return startOffset + (endOffset - startOffset) * Math.sin(t * Math.PI * 0.5);
case 'smooth':
default:
// Smooth hermite interpolation
const t2 = t * t;
const t3 = t2 * t;
return startOffset + (endOffset - startOffset) * (3 * t2 - 2 * t3);
}
}
createSplines() {
for (let groupIndex = 0; groupIndex < this.options.groupCount; groupIndex++) {
const group = new THREE.Group();
const splines = [];
for (let i = 0; i < this.options.splineCount; i++) {
const t = i / (this.options.splineCount - 1);
// Create distribution that's denser in the center
const normalizedOffset = (t - 0.2) * this.options.offset; // -1 to 1
const offset = normalizedOffset * this.options.spread;
const spline = this.createSingleSpline(offset, groupIndex, i);
splines.push({line: spline, offset});
group.add(spline);
}
this.splineGroups.push({
group,
splines,
baseOffset: groupIndex * 0.6,
phase: groupIndex * 0.8
});
this.scene.add(group);
}
}
createSingleSpline(offset, groupIndex, splineIndex) {
const points = this.generateSplinePoints(offset, groupIndex, splineIndex, 0);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
// Enhanced color calculation
const normalizedGroup = groupIndex / (this.options.groupCount - 1);
const normalizedSpline = splineIndex / (this.options.splineCount - 1);
const hue = this.options.hueBase +
normalizedGroup * this.options.hueVariation +
Math.sin(normalizedSpline * Math.PI * 2) * 0.05;
const saturation = this.options.saturation.min +
(this.options.saturation.max - this.options.saturation.min) *
(1 - Math.abs(offset) / this.options.spread);
const lightness = this.options.lightness.min +
(this.options.lightness.max - this.options.lightness.min) *
(1 - Math.abs(offset) / this.options.spread * 0.6);
const material = new THREE.LineBasicMaterial({
color: new THREE.Color().setHSL(hue, saturation, lightness),
transparent: true,
opacity: this.options.opacity * (1 - Math.abs(offset) / this.options.spread * 0.3),
linewidth: this.options.lineWidth
});
return new THREE.Line(geometry, material);
}
generateSplinePoints(offset, groupIndex, splineIndex, timeOffset = 0) {
const points = [];
const {segments} = this.options;
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const baseX = -(2 + this.options.canvasExtension) + t * (4 + 2 * this.options.canvasExtension);
let x = baseX;
let y = 0;
// Apply node influences with multiple wave frequencies
this.nodes.forEach((node, nodeIndex) => {
const distance = Math.abs(baseX - node.x);
const influence = Math.exp(-distance * distance * 2);
const animatedPhase = node.phase +
(this.time + timeOffset) * node.speed +
groupIndex * 0.2 +
splineIndex * 0.01;
const waveAmplitude = node.radius * influence;
// Multiple harmonics for complexity
this.options.waveFrequencies.forEach((freq, freqIndex) => {
const amplitude = this.options.waveAmplitudes[freqIndex] || 0.1;
const phaseMultiplier = 1 + freqIndex * 0.3;
y += Math.sin(t * Math.PI * freq + animatedPhase * phaseMultiplier) *
waveAmplitude * amplitude;
});
// Horizontal modulation for more organic feel
x += Math.cos(t * Math.PI * 6 + animatedPhase * 0.9) * waveAmplitude * 0.1;
});
// Apply spread offset with transition
const transitionOffset = this.getOffsetTransition(t);
y += offset * (1 + Math.sin(t * Math.PI * 2 + (this.time + timeOffset) * 0.5) * 0.3) +
transitionOffset;
points.push(new THREE.Vector3(x, y, 0));
}
return points;
}
updateSplineGeometry(splineData, groupIndex, splineIndex) {
const {line, offset} = splineData;
const points = this.generateSplinePoints(offset, groupIndex, splineIndex);
line.geometry.setFromPoints(points);
}
animate() {
this.time += this.options.animationSpeed;
// Animate nodes with boundary constraints
this.nodes.forEach((node, index) => {
node.x += Math.sin(this.time * 0.3 + index) * 0.002;
node.y += Math.cos(this.time * 0.4 + index * 1.3) * 0.002;
// Keep nodes within reasonable bounds
node.x = Math.max(-1.5, Math.min(1.5, node.x));
node.y = Math.max(-1, Math.min(1, node.y));
});
// Update spline groups
this.splineGroups.forEach((splineGroup, groupIndex) => {
const {group, splines, phase} = splineGroup;
// Subtle group transformations
group.position.y = Math.sin(this.time * 0.2 + phase) * 0.08;
group.position.x = Math.cos(this.time * 0.15 + phase) * 0.04;
group.rotation.z = Math.sin(this.time * 0.1 + phase) * 0.015;
// Update individual splines
splines.forEach((splineData, splineIndex) => {
this.updateSplineGeometry(splineData, groupIndex, splineIndex);
});
});
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(() => this.animate());
}
// Public methods for runtime configuration
updateOptions(newOptions) {
Object.assign(this.options, newOptions);
// Trigger rebuild if necessary
this.rebuild();
}
rebuild() {
// Clean up existing splines
this.splineGroups.forEach(({group}) => {
group.children.forEach(child => {
child.geometry.dispose();
child.material.dispose();
});
this.scene.remove(group);
});
this.splineGroups = [];
this.createSplines();
}
resize() {
const rect = this.canvas.getBoundingClientRect();
//this.canvas.width = rect.width * window.devicePixelRatio;
//this.canvas.height = rect.height * window.devicePixelRatio;
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
this.gl.uniform2f(this.u_resolution, this.canvas.width, this.canvas.height);
}
const width = this.canvas.clientWidth;
const height = this.canvas.clientHeight;
render() {
if (!this.gl) return;
this.renderer.setSize(width, height);
this.time += 0.016;
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
this.gl.uniform1f(this.u_time, this.time);
this.gl.uniform2f(this.u_mouse, this.mouseX, this.mouseY);
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
this.animationId = requestAnimationFrame(() => this.render());
const aspect = width / height;
this.camera.left = -2 * aspect;
this.camera.right = 2 * aspect;
this.camera.top = 1.5;
this.camera.bottom = -1.5;
this.camera.updateProjectionMatrix();
}
destroy() {
if (this.animationId) cancelAnimationFrame(this.animationId);
this.splineGroups.forEach(({group}) => {
group.children.forEach(child => {
child.geometry.dispose();
child.material.dispose();
});
});
this.renderer.dispose();
}
}
// Usage example with custom options
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById('aurora-canvas');
if (canvas) {
const aurora = new QuantumAurora(canvas);
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => aurora.resize(), 100);
const curtain = new GuillocheCurtain(canvas, {
spread: 7, // Wider spread
segments: 17,
lineWidth: 0.1, // Thicker lines
splineCount: 21, // More lines
groupCount: 2,
canvasExtension: 0.1,
offset: 0.1,
startOffset: -0.1, // Slight downward start
endOffset: 0.2, // Slight upward end
offsetTransition: 'smooth', // Smooth transition
animationSpeed: 0.001, // Slower animation
hueBase: 0.55, // More blue-green
hueVariation: 0.3,
opacity: 0.7
});
window.addEventListener('beforeunload', () => aurora.destroy());
window.addEventListener('beforeunload', () => curtain.destroy());
}
});