- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
335 lines
10 KiB
JavaScript
335 lines
10 KiB
JavaScript
// modules/canvas-animations/InteractiveEffects.js
|
|
import { CanvasManager } from './CanvasManager.js';
|
|
import { useEvent } from '../../core/useEvent.js';
|
|
import { Logger } from '../../core/logger.js';
|
|
|
|
/**
|
|
* Interactive Canvas Effects - Mouse/touch interactions, hover effects
|
|
*/
|
|
export const InteractiveEffects = {
|
|
|
|
/**
|
|
* Initialize interactive canvas
|
|
*/
|
|
init(canvas, config) {
|
|
const manager = new CanvasManager(canvas);
|
|
const effectType = config.effect || 'ripple';
|
|
|
|
const state = {
|
|
mouse: { x: 0, y: 0, isOver: false },
|
|
effects: [],
|
|
lastTime: 0
|
|
};
|
|
|
|
this.setupInteractionEvents(canvas, manager, state, config);
|
|
this.startAnimationLoop(manager, state, effectType, config);
|
|
|
|
Logger.info('[InteractiveEffects] Initialized with effect:', effectType);
|
|
},
|
|
|
|
/**
|
|
* Setup mouse/touch interaction events
|
|
*/
|
|
setupInteractionEvents(canvas, manager, state, config) {
|
|
// Mouse events
|
|
useEvent(canvas, 'mousemove', (e) => {
|
|
const pos = manager.getMousePosition(e);
|
|
state.mouse.x = pos.x;
|
|
state.mouse.y = pos.y;
|
|
|
|
if (config.effect === 'trail') {
|
|
this.addTrailPoint(state, pos.x, pos.y);
|
|
}
|
|
}, 'interactive-effects');
|
|
|
|
useEvent(canvas, 'mouseenter', () => {
|
|
state.mouse.isOver = true;
|
|
}, 'interactive-effects');
|
|
|
|
useEvent(canvas, 'mouseleave', () => {
|
|
state.mouse.isOver = false;
|
|
}, 'interactive-effects');
|
|
|
|
useEvent(canvas, 'click', (e) => {
|
|
const pos = manager.getMousePosition(e);
|
|
this.addClickEffect(state, pos.x, pos.y, config);
|
|
}, 'interactive-effects');
|
|
|
|
// Touch events for mobile
|
|
useEvent(canvas, 'touchstart', (e) => {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
const pos = manager.getMousePosition(touch);
|
|
this.addClickEffect(state, pos.x, pos.y, config);
|
|
}, 'interactive-effects');
|
|
|
|
useEvent(canvas, 'touchmove', (e) => {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
const pos = manager.getMousePosition(touch);
|
|
state.mouse.x = pos.x;
|
|
state.mouse.y = pos.y;
|
|
state.mouse.isOver = true;
|
|
|
|
if (config.effect === 'trail') {
|
|
this.addTrailPoint(state, pos.x, pos.y);
|
|
}
|
|
}, 'interactive-effects');
|
|
|
|
useEvent(canvas, 'touchend', () => {
|
|
state.mouse.isOver = false;
|
|
}, 'interactive-effects');
|
|
},
|
|
|
|
/**
|
|
* Start animation loop
|
|
*/
|
|
startAnimationLoop(manager, state, effectType, config) {
|
|
const animate = (timestamp) => {
|
|
const deltaTime = timestamp - state.lastTime;
|
|
state.lastTime = timestamp;
|
|
|
|
manager.clear();
|
|
|
|
switch (effectType) {
|
|
case 'ripple':
|
|
this.renderRippleEffect(manager, state, config);
|
|
break;
|
|
case 'trail':
|
|
this.renderTrailEffect(manager, state, config);
|
|
break;
|
|
case 'particles':
|
|
this.renderParticleEffect(manager, state, config, deltaTime);
|
|
break;
|
|
case 'magnetic':
|
|
this.renderMagneticEffect(manager, state, config);
|
|
break;
|
|
default:
|
|
this.renderRippleEffect(manager, state, config);
|
|
}
|
|
|
|
// Update effects
|
|
this.updateEffects(state.effects, deltaTime);
|
|
|
|
requestAnimationFrame(animate);
|
|
};
|
|
|
|
requestAnimationFrame(animate);
|
|
},
|
|
|
|
/**
|
|
* Add click effect (ripple, explosion, etc.)
|
|
*/
|
|
addClickEffect(state, x, y, config) {
|
|
const effect = {
|
|
x,
|
|
y,
|
|
age: 0,
|
|
maxAge: config.duration || 1000,
|
|
type: 'click',
|
|
intensity: config.intensity || 1
|
|
};
|
|
|
|
state.effects.push(effect);
|
|
},
|
|
|
|
/**
|
|
* Add trail point for mouse trail effect
|
|
*/
|
|
addTrailPoint(state, x, y) {
|
|
const point = {
|
|
x,
|
|
y,
|
|
age: 0,
|
|
maxAge: 500,
|
|
type: 'trail'
|
|
};
|
|
|
|
state.effects.push(point);
|
|
|
|
// Limit trail length
|
|
const trailLength = 20;
|
|
const trailPoints = state.effects.filter(e => e.type === 'trail');
|
|
if (trailPoints.length > trailLength) {
|
|
const oldestIndex = state.effects.indexOf(trailPoints[0]);
|
|
state.effects.splice(oldestIndex, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Render ripple effect
|
|
*/
|
|
renderRippleEffect(manager, state, config) {
|
|
const { ctx } = manager;
|
|
|
|
// Draw active ripples
|
|
state.effects.forEach(effect => {
|
|
if (effect.type === 'click') {
|
|
const progress = effect.age / effect.maxAge;
|
|
const radius = progress * (config.maxRadius || 100);
|
|
const opacity = (1 - progress) * 0.8;
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = opacity;
|
|
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 1)';
|
|
ctx.lineWidth = config.lineWidth || 3;
|
|
ctx.beginPath();
|
|
ctx.arc(effect.x / manager.options.pixelRatio, effect.y / manager.options.pixelRatio, radius, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
});
|
|
|
|
// Draw hover effect
|
|
if (state.mouse.isOver) {
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.3;
|
|
ctx.fillStyle = config.hoverColor || 'rgba(100, 150, 255, 0.3)';
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
state.mouse.x / manager.options.pixelRatio,
|
|
state.mouse.y / manager.options.pixelRatio,
|
|
config.hoverRadius || 30,
|
|
0,
|
|
Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Render trail effect
|
|
*/
|
|
renderTrailEffect(manager, state, config) {
|
|
const { ctx } = manager;
|
|
const trailPoints = state.effects.filter(e => e.type === 'trail');
|
|
|
|
if (trailPoints.length < 2) return;
|
|
|
|
ctx.save();
|
|
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.8)';
|
|
ctx.lineWidth = config.lineWidth || 5;
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
|
|
// Draw trail path
|
|
ctx.beginPath();
|
|
trailPoints.forEach((point, index) => {
|
|
const progress = 1 - (point.age / point.maxAge);
|
|
const x = point.x / manager.options.pixelRatio;
|
|
const y = point.y / manager.options.pixelRatio;
|
|
|
|
ctx.globalAlpha = progress * 0.8;
|
|
|
|
if (index === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
});
|
|
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Render particle effect
|
|
*/
|
|
renderParticleEffect(manager, state, config, deltaTime) {
|
|
const { ctx } = manager;
|
|
|
|
// Spawn particles on mouse move
|
|
if (state.mouse.isOver && Math.random() < 0.1) {
|
|
const particle = {
|
|
x: state.mouse.x,
|
|
y: state.mouse.y,
|
|
vx: (Math.random() - 0.5) * 4,
|
|
vy: (Math.random() - 0.5) * 4,
|
|
age: 0,
|
|
maxAge: 1000,
|
|
size: Math.random() * 5 + 2,
|
|
type: 'particle'
|
|
};
|
|
state.effects.push(particle);
|
|
}
|
|
|
|
// Update and draw particles
|
|
state.effects.forEach(effect => {
|
|
if (effect.type === 'particle') {
|
|
// Update position
|
|
effect.x += effect.vx;
|
|
effect.y += effect.vy;
|
|
effect.vy += 0.1; // Gravity
|
|
|
|
const progress = effect.age / effect.maxAge;
|
|
const opacity = (1 - progress) * 0.8;
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = opacity;
|
|
ctx.fillStyle = config.color || 'rgba(100, 150, 255, 1)';
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
effect.x / manager.options.pixelRatio,
|
|
effect.y / manager.options.pixelRatio,
|
|
effect.size * (1 - progress * 0.5),
|
|
0,
|
|
Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Render magnetic effect
|
|
*/
|
|
renderMagneticEffect(manager, state, config) {
|
|
const { ctx } = manager;
|
|
const { width, height } = manager.getSize();
|
|
|
|
if (!state.mouse.isOver) return;
|
|
|
|
// Draw magnetic field lines
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
const mouseX = state.mouse.x / manager.options.pixelRatio;
|
|
const mouseY = state.mouse.y / manager.options.pixelRatio;
|
|
|
|
ctx.save();
|
|
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.6)';
|
|
ctx.lineWidth = 2;
|
|
|
|
const lines = 8;
|
|
for (let i = 0; i < lines; i++) {
|
|
const angle = (i / lines) * Math.PI * 2;
|
|
const startX = centerX + Math.cos(angle) * 50;
|
|
const startY = centerY + Math.sin(angle) * 50;
|
|
|
|
// Curve towards mouse
|
|
const controlX = (startX + mouseX) / 2 + Math.sin(angle) * 30;
|
|
const controlY = (startY + mouseY) / 2 + Math.cos(angle) * 30;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(startX, startY);
|
|
ctx.quadraticCurveTo(controlX, controlY, mouseX, mouseY);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Update all effects (age and cleanup)
|
|
*/
|
|
updateEffects(effects, deltaTime) {
|
|
for (let i = effects.length - 1; i >= 0; i--) {
|
|
effects[i].age += deltaTime;
|
|
|
|
if (effects[i].age >= effects[i].maxAge) {
|
|
effects.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
}; |