update styles and layout; enhance footer design, adjust color classes, and implement Guilloche effect for improved aesthetics

This commit is contained in:
Michael Czechowski
2025-06-17 00:53:51 +02:00
parent 3cdf22eb09
commit 7595832888
10 changed files with 436 additions and 421 deletions

2
.pugrc
View File

@@ -44,7 +44,7 @@
},
"landingpage": {
"email": ["hello", "at", "M6C9.de"],
"name": ["Michael", "Czechowski"],
"name": "Mi&shy;cha&shy;el <wbr/><nobr>Czechowski</nobr>",
"logoSvg": "./src/assets/nls.svg",
"logoSvgInverted": "./src/assets/nls_inverted.svg",
"emojiSvg": "./src/assets/waving-hand.svg",

374
index.pug
View File

@@ -12,6 +12,8 @@ html.scroll-smooth(lang=head.lang)
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) => {
@@ -88,3 +90,375 @@ html.scroll-smooth(lang=head.lang)
);
});
});
// 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();
}
}

View File

@@ -3,7 +3,7 @@ include Title
include Container
section#academia
.grid.grid-cols-1.p-8.bg-white.text-nls-black(class="xl:grid-cols-2 xl:gap-36 xl:grid-cols-2 dark:bg-nls-black dark:text-white xl:p-20 min-h-[120vh]")
.grid.grid-cols-1.p-8.bg-deepwhite.text-nls-black(class="xl:grid-cols-2 xl:gap-36 xl:grid-cols-2 dark:bg-deepblack dark:text-white xl:p-20 sm:py-32 xl:py-48 min-h-[120vh]")
+Container
// region Skills
+Title("h2") !{academia.expertise.title}

View File

@@ -2,7 +2,7 @@ include Carousel
include Container
section#brands
.p-8.bg-nls-black.text-nls-white(class="sm:py-6")
.p-8.text-nls-white(class="bg-nls-black/90 dark:bg-nls-black sm:py-6")
.mt-20
+Carousel([...brands, ...brands, ...brands], {id: "brands-carousel", color: "black", category: "brands"})

View File

@@ -8,22 +8,22 @@ mixin Carousel(items, options)
// Pass through options even if not used
- const autoScroll = options.autoScroll ?? 4_800;
.carousel-container.relative.w-full.overflow-hidden.px-8(class="sm:px-12", id=id)
.carousel-container.relative.w-full.overflow-hidden-y.px-4(class="sm:px-12", id=id)
//- Navigation buttons
button.absolute.left-0.z-20.transform(
class=`-translate-y-1/2 top-1/2 sm:left-2 bg-white/40 hover:bg-white/80 text-nls-black rounded-full p-1 sm:p-3 shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-nls-${color}`,
button.absolute.z-20.transform(
class=`-translate-y-1/2 top-1/2 -left-6 sm:left-2 bg-white/40 hover:bg-white/80 text-nls-black rounded-full p-1 sm:p-3 shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-nls-${color}`,
aria-label="Previous slide",
onclick=`carouselPrev('${id}'); umami.track('carousel navigation', { direction: 'prev', category: '${category}', visitDuration: getVisitDuration() })`
)
svg.w-6.h-6.text-gray-800(class="sm:w-8 sm:h-8", fill="none", stroke="currentColor", viewBox="0 0 24 24")
svg.w-8.h-8.text-gray-800(class="sm:w-8 sm:h-8", fill="none", stroke="currentColor", viewBox="0 0 24 24")
path(stroke-linecap="round", stroke-linejoin="round", stroke-width="2", d="M15 19l-7-7 7-7")
button.absolute.right-0.z-20.transform(
class=`top-1/2 sm:right-2 -translate-y-1/2 bg-white/40 hover:bg-white/80 text-nls-black rounded-full p-1 sm:p-3 shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-nls-${color}`,
button.absolute.z-20.transform(
class=`top-1/2 -right-6 sm:right-2 -translate-y-1/2 bg-white/40 hover:bg-white/80 text-nls-black rounded-full p-1 sm:p-3 shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-nls-${color}`,
aria-label="Next slide",
onclick=`carouselNext('${id}'); umami.track('carousel navigation', { direction: 'next', category: '${category}', visitDuration: getVisitDuration() })`
)
svg.w-6.h-6.text-gray-800(class="sm:w-8 sm:h-8", fill="none", stroke="currentColor", viewBox="0 0 24 24")
svg.w-8.h-8.text-gray-800(class="sm:w-8 sm:h-8", fill="none", stroke="currentColor", viewBox="0 0 24 24")
path(stroke-linecap="round", stroke-linejoin="round", stroke-width="2", d="M9 5l7 7-7 7")
//- Carousel track

View File

@@ -20,12 +20,19 @@ mixin Svg
svg.w-24.h-24(class="sm:w-8 sm:h-8", aria-hidden="true", fill="currentColor", viewbox="0 0 24 24")
block
footer#footer.mt-24(class="border-t border-nls-black dark:border-nls-white")
footer#footer.pt-24.relative.overflow-hidden(class="w-min-h-screen")
// - Background canvas for Guilloche effect
canvas#footer-canvas.absolute.left-0.right-0.bottom-0.top-0.mx-auto.transition.w-full(class="-z-20 h-[100vh]")
//- Background gradient
.divider.absolute.left-0.right-0.top-0.h-full(class="-z-10 bg-gradient-to-t from-deepwhite/0 to-deepwhite dark:from-deepblack/0 dark:to-deepblack")
//- Footer content
.p-8.py-20.bg-white.text-nls-black(class="sm:py-36 dark:bg-nls-black dark:text-white sm:p-20")
.p-8.py-20.text-nls-black.z-20(class="sm:py-36 dark:text-white sm:p-20")
+Container
// region Contact
h2.text-4xl.mb-4 #{footer.title}
p.mb-8.max-w-prose(class="text-xl text-deepblack dark:text-deepwhite")
| !{landingpage.name}<br/>
span.opacity-60 !{landingpage.jobTitle[0]}
p.mb-8.max-w-prose #{footer.content}
.flex.flex-col.items-center.space-y-12.mb-8.gap-8(class="sm:flex-row sm:space-x-6 sm:space-y-0 sm:gap-2")
+Link(footer.githubUrl, "Visit GitHub profile", "_blank", "noopener noreferrer")
@@ -67,3 +74,29 @@ footer#footer.mt-24(class="border-t border-nls-black dark:border-nls-white")
//+Link("https://git.dailysh.it/nextlevelshit/dailysh.it", "Visit git repository (gitea)", "_blank", "noopener noreferrer")= footer.gitea
// endregion
script.
// Initialize with custom options
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById("footer-canvas");
if (canvas) {
const curtain = new GuillocheCurtain(canvas, {
spread: 5,
segments: 120,
lineWidth: 2,
splineCount: 22,
groupCount: 1,
canvasExtension: 0.3,
offset: 0.3,
startOffset: -0.2,
endOffset: 0.1,
offsetTransition: "sine",
animationSpeed: 0.002,
hueBase: 0.55,
hueVariation: 0.3,
opacity: 0.4,
});
window.addEventListener("beforeunload", () => curtain.destroy());
}
});

View File

@@ -76,10 +76,13 @@ head
"nls-red": "#E76F51",
"nls-orange": "#F9A03F",
"nls-yellow": "#F9C74F",
"nls-black": "#0f0f11",
"nls-white": "#f9f9f9",
"nls-black": "#0D0E11",
"nls-white": "#f8fafc",
white: "#f9f9f9",
black: "#0f0f11",
gray: "#6b7280",
deepblack: "#0a0a0c",
deepwhite: "#fefefe",
},
},
},
@@ -147,7 +150,7 @@ head
section ~ section,
section ~ footer,
section ~ header {
padding-top: 4rem;
/*padding-top: 4rem;*/
}
p a {

View File

@@ -1,12 +1,8 @@
header.bg-white.text-nls-black.relative.overflow-hidden(class="dark:text-white dark:bg-nls-black border-b border-nls-black dark:border-nls-white")
header.bg-nls-white.text-nls-black.relative.overflow-hidden(class="dark:text-white dark:bg-nls-black border-b border-nls-black/50 dark:border-nls-white/50")
.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.pointer-events-none
// Container for name with shadow effect
.relative.inline-block#name-container
h2.relative.z-10.tracking-normal.font-serif.text-2xl.font-semibold.opacity-100.transition(class="sm:text-3xl peer-hover:opacity-100", itemprop="name")
span.relative.mr-2#first-name= landingpage.name[0]
span.relative#last-name= landingpage.name[1]
h2.tracking-normal.font-serif.text-xl.font-semibold.opacity-100.transition(class="sm:text-2xl peer-hover:opacity-100", itemprop="name") !{landingpage.name}
h1.tracking-normal.text-md.mb-16.opacity-95.transition(class="sm:text-lg peer-hover:opacity-100", itemprop="jobTitle")
| !{landingpage.jobTitle[0]}
@@ -17,398 +13,7 @@ header.bg-white.text-nls-black.relative.overflow-hidden(class="dark:text-white d
canvas#aurora-canvas.absolute.z-0.left-0.right-0.bottom-0.top-0.mx-auto.transition.w-full(class="h-[100vh]")
script(src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js")
//script.
// // Shadow text injection - runs after DOM loads
// document.addEventListener('DOMContentLoaded', () => {
// const firstNameSpan = document.getElementById('first-name');
// const lastNameSpan = document.getElementById('last-name');
//
// // Rule: first letter + remaining letter count
// const firstName = '#{landingpage.name[0]}'; // "Michael"
// const lastName = '#{landingpage.name[1]}'; // "Czechowski"
//
// const generateShadowText = (word) => {
// const firstLetter = word.charAt(0);
// const remainingCount = word.length - 1;
// return firstLetter + remainingCount;
// };
//
// // Create shadow elements
// const createShadow = (text) => {
// const shadow = document.createElement('div');
// shadow.className = 'absolute text-5xl sm:text-7xl font-black opacity-15 pointer-events-none select-none -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2';
// shadow.textContent = text;
// return shadow;
// };
//
// // Generate M6 and C9 based on rule
// const m6Shadow = createShadow(generateShadowText(firstName)); // M6
// const c9Shadow = createShadow(generateShadowText(lastName)); // C9
//
// // Append shadows to their respective spans
// firstNameSpan.appendChild(m6Shadow);
// lastNameSpan.appendChild(c9Shadow);
// });
script.
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();
}
}
// Initialize with custom options
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById("aurora-canvas");
@@ -431,11 +36,11 @@ header.bg-white.text-nls-black.relative.overflow-hidden(class="dark:text-white d
});
window.addEventListener("beforeunload", () => curtain.destroy());
window.addEventListener("resize", (e) => {
// only resize if width has changed
if (e.target.innerWidth !== curtain.containerWidth) {
curtain.resize();
}
});
// window.addEventListener("resize", (e) => {
// // only resize if width has changed
// if (e.target.innerWidth !== curtain.containerWidth) {
// curtain.resize();
// }
// });
}
});

View File

@@ -2,12 +2,12 @@ include Carousel
include Container
include Title
section#portfolio(class="border-t border-nls-black dark:border-nls-white")
section#portfolio(class="border-t border-nls-black/50 dark:border-nls-white/50")
.p-8.bg-nls-white.text-nls-black(class="dark:bg-nls-black dark:text-white sm:py-32")
+Container
+Title("h2", true)
| Portfolio
p.text-center.pb-12
p.text-center.pb-12.font-serif
| Free and Open Source Software, Customer Projects and other useful Applications
.max-w-screen.mx-auto.pb-32(class="sm:max-w-80vw")
+Carousel(portfolio, {id: "portfolio-carousel", color: "black", category: "portfolio", slideClasses: "w-full", autoScroll: false, color: "violet"})

View File

@@ -2,7 +2,7 @@ include Collapsable
include Title
include Container
section#professional.bg-white.text-nls-black(class="dark:bg-nls-black dark:text-white")
section#professional.bg-nls-white.text-nls-black(class="dark:bg-nls-black dark:text-white")
.p-8(class="sm:p-20")
+Container
+Title("h2") !{professional.title}