This repository has been archived on 2025-10-29. You can view files and clone it, but cannot push or open issues or pull requests.
Files
M6C9.de/index.pug

450 lines
15 KiB
Plaintext

doctype html
html.scroll-smooth(lang=head.lang)
include src/components/Head
body.m-0.p-0.bg-nls-white.text-nls-black(class="dark:bg-nls-black dark:text-white")
main.flex.flex-col
article(itemscope, itemtype="http://schema.org/Person")
include src/components/Landingpage
include src/components/Professional
include src/components/Portfolio
include src/components/Brands
include src/components/Academia
include src/components/Footer
script(src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js")
script.
const footerEl = document.querySelector("#footer");
const footerObserver = new IntersectionObserver((payload) => {
const hasScrolledTo = payload.pop().isIntersecting;
if (hasScrolledTo && window.hasOwnProperty("umami")) {
umami.track("scrolled to", {position: "footer", id: "footer", visitDuration: getVisitDuration()});
console.debug("scrolled to footer", {visitDuration: getVisitDuration()});
}
});
footerObserver.observe(footerEl);
const sections = document.querySelectorAll("section");
const sectionObserver = (id) =>
new IntersectionObserver((payload) => {
const hasScrolledTo = payload.pop().isIntersecting;
if (hasScrolledTo && window.hasOwnProperty("umami")) {
umami.track("scrolled to", {position: "section", id, visitDuration: getVisitDuration()});
console.debug("scrolled to section with id", {id, visitDuation: getVisitDuration()});
}
});
sections.forEach((section) => sectionObserver(section.id).observe(section));
// carousel.js - Just the essentials
function carouselPrev(carouselId) {
const track = document.querySelector(`#${carouselId} .carousel-track`);
const slideWidth = track.querySelector(".carousel-slide").offsetWidth;
track.scrollBy({left: -slideWidth, behavior: "smooth"});
updateCarouselDots(carouselId);
}
function carouselNext(carouselId) {
const track = document.querySelector(`#${carouselId} .carousel-track`);
const slideWidth = track.querySelector(".carousel-slide").offsetWidth;
track.scrollBy({left: slideWidth, behavior: "smooth"});
updateCarouselDots(carouselId);
}
function carouselGoTo(carouselId, index) {
const track = document.querySelector(`#${carouselId} .carousel-track`);
const slideWidth = track.querySelector(".carousel-slide").offsetWidth;
track.scrollTo({left: slideWidth * index, behavior: "smooth"});
updateCarouselDots(carouselId);
}
function updateCarouselDots(carouselId) {
// Debounced dot state update
clearTimeout(window.carouselDotTimeout);
window.carouselDotTimeout = setTimeout(() => {
const track = document.querySelector(`#${carouselId} .carousel-track`);
const dots = document.querySelectorAll(`#${carouselId} .carousel-dot`);
const slideWidth = track.querySelector(".carousel-slide").offsetWidth;
const currentIndex = Math.round(track.scrollLeft / slideWidth);
dots.forEach((dot, index) => {
if (index === currentIndex) {
dot.className = dot.className.replace(/bg-gray-\d+/, "bg-nls-orange");
} else {
dot.className = dot.className.replace(/bg-nls-\w+/, "bg-gray-300 dark:bg-gray-600");
}
});
}, 100);
}
// Auto-initialize all carousels
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".carousel-track").forEach((track) => {
track.addEventListener(
"scroll",
() => {
const carouselId = track.closest(".carousel-container").id;
updateCarouselDots(carouselId);
},
{passive: true},
);
});
});
// Guilloche Curtain
class GuillocheCurtain {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.containerHeight = window.innerHeight;
this.containerWidth = window.innerWidth;
this.options = {
// Visual parameters
spread: 0.25,
lineWidth: 0.8,
opacity: 0.6,
splineCount: 40,
groupCount: 6,
// Animation parameters
animationSpeed: 0.005,
nodeCount: 7,
nodeSpeed: {min: 0.3, max: 0.8},
// Color parameters
hueBase: 0.55,
hueVariation: 0.1,
saturation: {min: 0.3, max: 0.7},
lightness: {min: 0.25, max: 0.65},
// Spline parameters
segments: 100,
offset: 5,
startOffset: 0.0,
endOffset: 0.0,
offsetTransition: "smooth",
canvasExtension: 1.0,
// Wave complexity
waveFrequencies: [1, 2, 3, 5, 8, 13],
waveAmplitudes: [0.4, 0.25, 0.15, 0.1, 0.02, 0.001],
...options,
};
// Interaction state
this.mouse = {
x: 0,
y: 0,
normalizedX: 0.5, // 0-1 range
normalizedY: 0.5, // 0-1 range
};
// Dynamic parameters that respond to interaction
this.dynamic = {
hueBase: this.options.hueBase,
saturation: this.options.saturation,
lightness: this.options.lightness,
};
// Smoothing for parameter transitions
this.smoothing = {
parameterLerp: 0.05,
};
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,
alpha: true,
antialias: true,
});
this.time = 0;
this.splineGroups = [];
this.nodes = [];
this.init();
this.setupInteraction();
this.createNodes();
this.createSplines();
this.animate();
}
init() {
this.renderer.setSize(this.containerWidth, this.containerHeight);
this.renderer.setClearColor(0x000000, 0);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.camera.position.z = 1;
}
setupInteraction() {
const updateMousePosition = (clientX, clientY) => {
const rect = this.canvas.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
// Update current position
this.mouse.x = x;
this.mouse.y = y;
// Normalize to 0-1 range
this.mouse.normalizedX = Math.max(0, Math.min(1, x / this.canvas.clientWidth));
this.mouse.normalizedY = Math.max(0, Math.min(1, y / this.canvas.clientHeight));
};
// Mouse events
this.canvas.addEventListener("mousemove", (e) => {
updateMousePosition(e.clientX, e.clientY);
});
// Mouse enter/leave for graceful transitions
this.canvas.addEventListener("mouseenter", () => {
// Smooth entrance - no abrupt changes needed
});
this.canvas.addEventListener("mouseleave", () => {
// Gradually return to center when mouse leaves
this.mouse.normalizedX = 0.5;
this.mouse.normalizedY = 0.5;
});
}
updateDynamicParameters() {
const {normalizedX, normalizedY} = this.mouse;
const {parameterLerp} = this.smoothing;
// Color temperature interaction (Implementation 6)
// X-axis: Cool blues (left) → Warm oranges (right)
// Y-axis: Affects saturation and lightness
// Hue mapping: 0.6 (blue) to 0.1 (orange) based on X position
const targetHue = 0.6 - normalizedX * 0.5; // Maps 0.6 → 0.1
this.dynamic.hueBase = this.dynamic.hueBase * (1 - parameterLerp) + targetHue * parameterLerp;
// Y position affects saturation (top = high saturation, bottom = low)
const targetSatMin = 0.2 + (1 - normalizedY) * 0.4; // 0.2-0.6 range
const targetSatMax = 0.5 + (1 - normalizedY) * 0.4; // 0.5-0.9 range
this.dynamic.saturation.min = this.dynamic.saturation.min * (1 - parameterLerp) + targetSatMin * parameterLerp;
this.dynamic.saturation.max = this.dynamic.saturation.max * (1 - parameterLerp) + targetSatMax * parameterLerp;
// Y position affects lightness (top = bright, bottom = dark)
const targetLightMin = 0.15 + (1 - normalizedY) * 0.3; // 0.15-0.45 range
const targetLightMax = 0.45 + (1 - normalizedY) * 0.4; // 0.45-0.85 range
this.dynamic.lightness.min = this.dynamic.lightness.min * (1 - parameterLerp) + targetLightMin * parameterLerp;
this.dynamic.lightness.max = this.dynamic.lightness.max * (1 - parameterLerp) + targetLightMax * parameterLerp;
}
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),
});
}
}
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:
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);
const normalizedOffset = (t - 0.2) * this.options.offset;
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 using dynamic parameters
const normalizedGroup = groupIndex / (this.options.groupCount - 1);
const normalizedSpline = splineIndex / (this.options.splineCount - 1);
const hue = this.dynamic.hueBase + normalizedGroup * this.options.hueVariation + Math.sin(normalizedSpline * Math.PI * 2) * 0.05;
const saturation = this.dynamic.saturation.min + (this.dynamic.saturation.max - this.dynamic.saturation.min) * (1 - Math.abs(offset) / this.options.spread);
const lightness = this.dynamic.lightness.min + (this.dynamic.lightness.max - this.dynamic.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 / ((this.options.splineCount - splineIndex) * 0.1)) * (1 - (Math.abs(offset) / this.options.spread) * 0.1),
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 original wave amplitudes
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;
// Use original static amplitudes
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;
});
x += Math.cos(t * Math.PI * 6 + animatedPhase * 0.9) * waveAmplitude * 0.1;
});
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);
// Update material colors based on dynamic parameters
const normalizedGroup = groupIndex / (this.options.groupCount - 1);
const normalizedSpline = splineIndex / (this.options.splineCount - 1);
const hue = this.dynamic.hueBase + normalizedGroup * this.options.hueVariation + Math.sin(normalizedSpline * Math.PI * 2) * 0.05;
const saturation = this.dynamic.saturation.min + (this.dynamic.saturation.max - this.dynamic.saturation.min) * (1 - Math.abs(offset) / this.options.spread);
const lightness = this.dynamic.lightness.min + (this.dynamic.lightness.max - this.dynamic.lightness.min) * (1 - (Math.abs(offset) / this.options.spread) * 0.6);
line.material.color.setHSL(hue, saturation, lightness);
}
animate() {
// Update dynamic parameters based on mouse interaction
this.updateDynamicParameters();
// Use original static animation speed
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;
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;
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;
splines.forEach((splineData, splineIndex) => {
this.updateSplineGeometry(splineData, groupIndex, splineIndex);
});
});
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(() => this.animate());
}
updateOptions(newOptions) {
Object.assign(this.options, newOptions);
this.rebuild();
}
rebuild() {
this.splineGroups.forEach(({group}) => {
group.children.forEach((child) => {
child.geometry.dispose();
child.material.dispose();
});
this.scene.remove(group);
});
this.splineGroups = [];
this.createSplines();
}
resize() {
console.log("Resizing canvas and updating camera");
const width = window.innerWidth;
const height = window.innerHeight;
this.renderer.setSize(width, height);
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() {
this.splineGroups.forEach(({group}) => {
group.children.forEach((child) => {
child.geometry.dispose();
child.material.dispose();
});
});
this.renderer.dispose();
}
}