update styles and layout; enhance footer design, adjust color classes, and implement Guilloche effect for improved aesthetics
This commit is contained in:
2
.pugrc
2
.pugrc
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"landingpage": {
|
||||
"email": ["hello", "at", "M6C9.de"],
|
||||
"name": ["Michael", "Czechowski"],
|
||||
"name": "Mi­cha­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
374
index.pug
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user