add mouse interaction; implement dynamic color parameters and improve animation responsiveness
This commit is contained in:
@@ -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());
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user