add mouse interaction; implement dynamic color parameters and improve animation responsiveness

This commit is contained in:
Michael Czechowski
2025-06-14 14:41:12 +02:00
parent 61df9dc052
commit 535052ecf7

View File

@@ -21,38 +21,58 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
this.canvas = canvas;
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
spread: 0.25,
lineWidth: 0.8,
opacity: 0.6,
splineCount: 40,
groupCount: 6,
// Animation parameters
animationSpeed: 0.005, // Global time multiplier
nodeCount: 7, // Attractor nodes
animationSpeed: 0.005,
nodeCount: 7,
nodeSpeed: {min: 0.3, max: 0.8},
// Color parameters
hueBase: 0.55, // Base hue (cyan-ish)
hueVariation: 0.1, // Color spread
hueBase: 0.55,
hueVariation: 0.1,
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)
segments: 100,
offset: 5,
startOffset: 0.0,
endOffset: 0.0,
offsetTransition: "smooth",
canvasExtension: 1.0,
// Wave complexity
waveFrequencies: [1, 2, 3, 5, 8, 13], // Multiple harmonics
waveAmplitudes: [0.4, 0.25, 0.15, 0.1, 0.02, 0.001], // Corresponding amplitudes
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({
@@ -66,6 +86,7 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
this.nodes = [];
this.init();
this.setupInteraction();
this.createNodes();
this.createSplines();
this.animate();
@@ -78,6 +99,78 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
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);
});
// Touch events
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (e.touches.length > 0) {
updateMousePosition(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: false });
// 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({
@@ -90,7 +183,6 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
}
}
// Calculate offset transition from start to end
getOffsetTransition(t) {
const {startOffset, endOffset, offsetTransition} = this.options;
@@ -101,7 +193,6 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
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);
@@ -115,8 +206,7 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
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 normalizedOffset = (t - 0.2) * this.options.offset;
const offset = normalizedOffset * this.options.spread;
const spline = this.createSingleSpline(offset, groupIndex, i);
@@ -138,22 +228,26 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
const points = this.generateSplinePoints(offset, groupIndex, splineIndex, 0);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
// Enhanced color calculation
// Enhanced color calculation using dynamic parameters
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 hue = this.dynamic.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 saturation = this.dynamic.saturation.min +
(this.dynamic.saturation.max - this.dynamic.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 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,
// Add shading for depth
opacity: (this.options.opacity / (splineIndex / 6)) * (1 - (Math.abs(offset) / this.options.spread) * 0.1),
//opacity: this.options.opacity * (1 - Math.abs(offset) / this.options.spread * 0.1),
opacity: (this.options.opacity / (splineIndex / 6)) *
(1 - (Math.abs(offset) / this.options.spread) * 0.1),
linewidth: this.options.lineWidth,
});
@@ -171,30 +265,31 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
let x = baseX;
let y = 0;
// Apply node influences with multiple wave frequencies
// 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 animatedPhase = node.phase + (this.time + timeOffset) * node.speed +
groupIndex * 0.2 + splineIndex * 0.01;
const waveAmplitude = node.radius * influence;
// Multiple harmonics for complexity
// 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;
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;
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));
}
@@ -206,9 +301,30 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
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
@@ -216,7 +332,6 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
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));
});
@@ -225,12 +340,10 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
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);
});
@@ -240,15 +353,12 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
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();
@@ -287,23 +397,23 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
}
}
// Usage example with custom options
// Initialize with custom options
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById("aurora-canvas");
if (canvas) {
const curtain = new GuillocheCurtain(canvas, {
spread: 7, // Wider spread
spread: 7,
segments: 222,
lineWidth: 2, // Thicker lines
splineCount: 12, // More lines
lineWidth: 2,
splineCount: 12,
groupCount: 2,
canvasExtension: 0.3,
offset: 0.4,
startOffset: -0.6, // Slight downward start
endOffset: 0.4, // Slight upward end
offsetTransition: "sine", // Smooth transition
animationSpeed: 0.001, // Slower animation
hueBase: 0.55, // More blue-green
startOffset: -0.6,
endOffset: 0.4,
offsetTransition: "sine",
animationSpeed: 0.001,
hueBase: 0.55,
hueVariation: 0.3,
opacity: 0.3,
});
@@ -311,4 +421,4 @@ header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black
window.addEventListener("beforeunload", () => curtain.destroy());
window.addEventListener("resize", () => curtain.resize());
}
});
});